//
// Корзина хранится в localstorage для всех пользователей.
// Ключом для доступа к данным корзины конкретного пользователя служит его _id,
// полученный из localStorage.authToken после авторизации.
// Данные корзины для каждого пользователя сбрасываются только в момент покупки.
// Наполнять корзину можно и анонимно. Но оформить этот заказ не получится.
// В случае развития этого проекта можно организовать дополнительную логику для
// анонимной корзины. Например: если после авторизации Ваша корзина из истории
// оказалась пустой, то товары из анонимной корзины попадут в Вашу, но только в случае
// если сессия не прерывалась.
// Но мне кажется, что корзина должна быть организована на серверной стороне.
//
const delay = (ms) => new Promise((res) => setTimeout(() => res(ms), ms));
// localStorage.removeItem("authToken");
// "eyJzdWIiOnsibG9naW4iOiJfX2Fub255bW91c19fIn19"-- - {"sub":{"login": "__anonymous__"}}
const urlConst = "http://shop-roles.asmer.fs.a-level.com.ua";
const defaultLoginName = "__anonymous__";
const defaultLoginId = "0";
let loginName = defaultLoginName;
let loginId = defaultLoginId;
let basketObj = {};
basketObj[defaultLoginId] = {};
goLogin.onclick = () => {
init();
loginForm.style.display = "";
};
goLogoff.onclick = () => {
localStorage.removeItem("authToken");
loginName = defaultLoginName;
init();
};
goRegistration.onclick = () => {
init();
registrationForm.style.display = "";
};
async function setLoginFromToken() {
loginName = defaultLoginName;
loginId = defaultLoginId;
try {
loginName = await JSON.parse(atob(localStorage.authToken.split(".")[1])).sub.login;
loginId = await JSON.parse(atob(localStorage.authToken.split(".")[1])).sub.id;
updateBasketObj();
} catch (error) {
console.log("Ошибка декодирования login из токена / либо пользователь разлогинился: ", error);
}
}
const init = function () {
if (!localStorage.basket) {
localStorage.basket = JSON.stringify(basketObj);
} else {
basketObj = JSON.parse(localStorage.basket);
}
registrationForm.style.display = "none";
loginForm.style.display = "none";
forImage.style.display = "none";
forBasket.style.display = "none";
checkAuthToken();
};
async function checkAuthToken() {
if (localStorage.authToken) {
goLogin.style.display = "none";
goRegistration.style.display = "none";
goLogoff.style.display = "";
historyDiv.style.display = "";
nickNameDiv.style.display = "";
await setLoginFromToken();
nickNameSpan.innerHTML = loginName;
return true;
}
goLogin.style.display = "";
goRegistration.style.display = "";
goLogoff.style.display = "none";
historyDiv.style.display = "none";
nickNameSpan.innerHTML = defaultLoginName;
nickNameDiv.style.display = "none";
setLoginFromToken();
return false;
}
async function updateBasketObj() {
if (!basketObj[loginId]) {
basketObj[loginId] = {};
}
try {
let temp = await JSON.stringify(basketObj);
localStorage.basket = temp;
} catch (error) {
console.log("Ошибка localStorage.basket = JSON.stringify(basketObj) - ", error);
}
}
const getGQL = (url) => (query, variables = {}) => {
return fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
...(localStorage.authToken ? { Authorization: `Bearer ${localStorage.authToken}` } : {}),
},
body: JSON.stringify({ query, variables }),
})
.then((res) => res.json())
.catch((err) => console.log(err));
};
const gql = getGQL(urlConst + `/graphql`);
async function categories(parentEl = leftSide, parentID = null) {
let result = await gql(
`query categories ($query:String){
CategoryFind(query:$query){
_id
name
}
}`,
{
query: JSON.stringify([{ "parent._id": parentID }, { sort: [{ name: 1 }] }]),
}
);
if (result.errors) return;
let ul = document.createElement("ul");
parentEl.append(ul);
for (let { name, _id } of result.data.CategoryFind) {
let li = document.createElement("li");
li.innerText = name;
li.style.fontWeight = "";
let loaded;
li.onclick = (event) => {
if (event) event.stopPropagation();
li.parentElement.parentElement.style.fontWeight = "";
[].forEach.call(li.parentElement.children, (el) => (el.style.fontWeight = ""));
li.style.fontWeight = "700";
if (!loaded) {
categories(li, _id);
loaded = true;
} else {
loaded = false;
[].forEach.call(li.children, (el) => el.remove());
}
mainBlock.innerText = "";
categoryTitle.innerText = "";
subMenu.innerText = "";
showAllInCategory(_id, name);
};
ul.append(li);
let span = document.createElement("span");
span.innerText = name;
subMenu.append(span);
span.onclick = () => {
li.onclick();
};
}
}
categories();
function showAllInCategory(_id, name) {
let h = document.createElement("h1");
h.append(name);
categoryTitle.append(h);
categoryTitle.append(`_${_id} - id категории`);
showGoodsInCategory(mainBlock, _id);
showAllGoodsInAllSubcategories(mainBlock, _id);
}
async function showGoodsInCategory(parentEl, _id) {
let result = await gql(
`query GoodsFromCatSort ($sort:String){
GoodFind(query: $sort) {
_id
name
description
price
images {
_id
createdAt
text
url
originalFileName
}
}
}`,
{
sort: JSON.stringify([{ "categories._id": _id }, { sort: [{ name: 1 }] }]),
}
);
if (result.errors) return;
for (let { name, _id, description, price, images } of result.data.GoodFind) {
let shelfToker = document.createElement("div");
shelfToker.classList.add("shelfToker");
shelfToker.onclick = (e) => {
e.stopPropagation();
shelfToker.classList.add("shelfTokerBig");
};
parentEl.append(shelfToker);
let h3 = document.createElement("h3");
h3.append(name);
shelfToker.append(h3);
h3 = document.createElement("h3");
h3.append("$ " + price);
shelfToker.append(h3);
let p = document.createElement("p");
let h4 = document.createElement("h4");
h4.append("Описание: ");
p.append(h4);
p.append(description);
shelfToker.append(p);
let divImg = document.createElement("div");
divImg.classList.add("divImg");
for (let img of images) {
let divImgOne = document.createElement("div");
divImgOne.classList.add("divImgOne");
let imgNode = document.createElement("img");
imgNode.src = urlConst + `/` + img.url;
divImgOne.append(imgNode);
divImg.append(divImgOne);
let namberOfImg = 0;
imgNode.onclick = (e) => {
e.stopPropagation();
forImage.style.display = "";
forImgSrc.src = urlConst + `/` + images[namberOfImg].url;
forImage.onclick = () => {
forImgSrc.src = urlConst + `/` + images[namberOfImg++ % (images.lenght ? images.lenght : 1)].url;
};
let imgKeyEsc = (ev) => {
if (ev.code === "Escape") {
forImage.style.display = "none";
window.removeEventListener("keydown", imgKeyEsc);
}
};
window.addEventListener("keydown", imgKeyEsc);
};
}
shelfToker.append(divImg);
let count = document.createElement("input");
count.setAttribute("type", "number");
count.setAttribute("min", "1");
count.value = "1";
count.onclick = (e) => {
e.stopPropagation();
};
shelfToker.append(count);
let putInBasketBtn = document.createElement("button");
putInBasketBtn.append("Добавить в корзину");
putInBasketBtn.classList.add("putInBasketBtn");
putInBasketBtn.onclick = (e) => {
e.stopPropagation();
addToBasket(_id, count.value);
};
shelfToker.append(putInBasketBtn);
shelfToker.insertAdjacentHTML("beforeEnd", `
${_id} - id товара
`);
let shelfTokerExitBtn = appendActionBtn(shelfToker, { onTop: false }, "Вернуться назад");
shelfTokerExitBtn.classList.add("shelfTokerExitBtn");
}
}
function addToBasket(_idValue, countValue) {
basketObj[loginId][_idValue] = +countValue;
updateBasketObj();
alert(`Товар добавлен в корзину.`);
}
async function showAllGoodsInAllSubcategories(parentEl, catId) {
let result = await gql(
`query subCategories ($subcat:String){
CategoryFind(query: $subcat) {
name
_id
}
}`,
{
subcat: JSON.stringify([{ "parent._id": catId }, { sort: [{ name: 1 }] }]),
}
);
if (result.errors) return;
for (let { _id } of result.data.CategoryFind) {
showGoodsInCategory(parentEl, _id);
showAllGoodsInAllSubcategories(parentEl, _id);
}
}
function CreateInputField(parentNode, hidden = false, isCheckNeed = false) {
let inpEl = document.createElement("input");
inpEl.setAttribute("type", hidden ? "password" : "text");
inpEl.setAttribute("placeholder", hidden ? "Password" : "Login");
inpEl.oninput = (isUser = true) => {
if (isUser && this.onChange && typeof this.onChange === "function") {
this.onChange(inpEl.value);
}
};
parentNode.append(inpEl);
let div = document.createElement("div");
let checkBox = document.createElement("input");
checkBox.setAttribute("type", "checkbox");
div.append(checkBox);
let seePassword = document.createElement("span");
seePassword.append("Показать пароль");
div.append(seePassword);
checkBox.oninput = () => {
if (checkBox.checked) {
inpEl.setAttribute("type", "text");
} else inpEl.setAttribute("type", "password");
};
inpEl.addEventListener("keydown", (e) => {
if (["Enter", "NumpadEnter"].includes(e.code)) {
this.onEnter();
}
});
if (isCheckNeed) {
parentNode.append(div);
}
this.getValue = function () {
return inpEl.value;
};
this.getCheckStatus = function () {
return checkBox.checked;
};
this.setValue = function (value = "") {
inpEl.value = value;
};
this.setCheckBox = function (value = false) {
checkBox.checked = value;
};
this.onChange = () => {};
this.onEnter = () => {};
}
function CreateLoginForm(parentNode, isModeLogin = true) {
//isModeLogin = true - по умолчанию создается форма для логина
// false - будет сождаваться форма для регистрации
let loginField = new CreateInputField(parentNode);
let passwordField = new CreateInputField(parentNode, true, true);
// function CreateInputField(parentNode, hidden = false, isCheckNeed = false)
let loginButton = document.createElement("button");
loginButton.append(isModeLogin ? "Войти" : "Зарегистрироваться");
loginButton.setAttribute("disabled", "disabled");
parentNode.append(loginButton);
let canselButton = document.createElement("button");
canselButton.append("Отмена");
parentNode.append(canselButton);
loginField.onChange = passwordField.onChange = () => {
if (loginField.getValue() && passwordField.getValue()) {
loginButton.removeAttribute("disabled");
} else loginButton.setAttribute("disabled", "disabled");
};
loginField.onEnter = passwordField.onEnter = () => loginButton.onclick();
canselButton.onclick = () => {
this.clearAndClose();
};
loginButton.onclick = () => {
if (this.submit && typeof this.submit === "function") {
let loginInfo = {
login: loginField.getValue(),
password: passwordField.getValue(),
};
this.submit(loginInfo);
}
};
this.submit = () => {};
this.clearAndClose = function () {
loginField.setValue();
passwordField.setValue();
passwordField.setCheckBox();
parentNode.style.display = "none";
};
}
let loginFormObject = new CreateLoginForm(loginForm);
let registrationFormObject = new CreateLoginForm(registrationForm, false);
loginFormObject.submit = (loginInfo) => {
loginToDB(loginInfo);
};
registrationFormObject.submit = (reginInfo) => {
registrToDB(reginInfo);
};
async function loginToDB({ login, password } = {}) {
let result = await gql(
`query login($login: String, $password: String) {
login(login: $login, password: $password)
}`,
{ login, password }
);
if (result.errors) {
alert("Ошибка сети");
return;
}
if (result.data.login) {
localStorage.authToken = result.data.login;
loginFormObject.clearAndClose();
loginForm.style.display = "none";
registrationFormObject.clearAndClose();
registrationForm.style.display = "none";
checkAuthToken();
} else alert("Ошибка!\nВведите правильные логин и пароль.");
}
async function registrToDB({ login, password } = {}) {
let result = await gql(
`mutation newUser($login: String, $password: String) {
UserUpsert(user: {login: $login, password: $password}) {
_id
createdAt
}
}`,
{ login, password }
);
if (result.errors) {
alert(`Ошибка регистрации:\n${result.errors[0].message}`);
return;
}
alert("Вы успешно зарегистрированы.");
loginToDB({ login, password });
}
historyDiv.onclick = () => {
showOrderHistory(forBasket);
};
async function showOrderHistory(parentNode) {
let result = await gql(
`query FindOrders($lookOrders2:String){
OrderFind(query:$lookOrders2){
total
orderGoods{
good{
name
images{
url
}
}
price
count
total
}
}
}`,
{ lookOrders2: JSON.stringify([{}]) }
// { lookOrders2: JSON.stringify([{}, { sort: [{ total: 1 }] }]) }
// ++++++++++++++ а вот эта сортировка ну никак не работает...??? +++++++++
// потому что total - вычисляемое поле
);
if (result.errors) {
alert("Ошибка сервера...");
console.log(result.errors);
return;
}
parentNode.style.display = "";
let masterTotal = 0;
forBasket.innerHTML = "";
appendActionBtn(forBasket, { onTop: true }, "Вернуться назад");
result.data.OrderFind.forEach((order) => {
let div1order = document.createElement("div");
forBasket.prepend(div1order);
showOrder(div1order, order);
let h = document.createElement("h4");
div1order.append(h);
h.append(`Всего за заказ: $${order.total}`);
masterTotal += order.total;
});
h = document.createElement("h2");
forBasket.prepend(h);
h.append(`Общая сумма всех заказов: $${masterTotal}`);
appendActionBtn(forBasket, { onTop: true }, "Вернуться назад");
forBasket.scrollTop = 0;
}
const appendActionBtn = function (parent, { onTop = false }, innerText = "Вернуться назад") {
let exitBtn = document.createElement("button");
exitBtn.append(innerText);
if (onTop) {
parent.prepend(exitBtn);
} else {
parent.append(exitBtn);
}
exitBtn.onclick = () => {
parent.style.display = "none";
parent.innerHTML = "";
};
return exitBtn;
};
const showOrder = function (parent, { orderGoods: orderArray, total: total1Order }) {
for (let { good, price, count, total: total1pozition } of orderArray) {
if (!good) {
good = {
images: [
{
url: "",
},
],
name: "",
};
}
// да, это костыль, но увы не смог составить запрос, чтобы вложенное поле
// в обекте - в массиве не равнялось null
// И вообще! Это ли не сервер должен следить, чтобы в базу чушь не заносили?
div = document.createElement("div");
parent.append(div);
let img = document.createElement("img");
div.append(img);
img.src = urlConst + `/` + good.images[0].url;
let p = document.createElement("p");
div.append(p);
p.append(good.name);
p = document.createElement("p");
div.append(p);
p.append(`Цена: $${price}`);
p = document.createElement("p");
div.append(p);
p.append(`Кол-во: ${count}`);
p = document.createElement("p");
div.append(p);
p.append(`Всего: $${total1pozition}`);
}
};
basketLogo.onclick = () => {
showBasket(forBasket);
};
async function showBasket(parent) {
parent.style.display = "";
parent.innerHTML = "";
appendActionBtn(parent, { onTop: true }, "Вернуться назад");
if (!Object.keys(basketObj[loginId]).length) {
let h = document.createElement("h3");
h.append("Ваша корзина пустая");
parent.append(h);
} else {
for (let [_id, count] of Object.entries(basketObj[loginId])) {
await show1goodFromBasket(parent, _id, +count);
}
let hTotal = document.createElement("h3");
hTotal.setAttribute("id", "hTotal");
parent.append(hTotal);
CountTotal(parent);
}
appendActionBtn(parent, { onTop: false }, "Вернуться назад");
let btn = appendActionBtn(parent, { onTop: false }, "Оформить заказ (купить)");
btn.setAttribute("id", "buyBtn");
if (!Object.keys(basketObj[loginId]).length || loginId === "0") {
btn.setAttribute("disabled", "disabled");
}
btn.onclick = async function () {
parent.style.display = "none";
parent.innerHTML = "";
await buy();
};
if (loginId === "0") {
let h = document.createElement("h3");
h.append("Для оформления заказа необходимо авторизоваться");
parent.append(h);
}
}
const CountTotal = function (parent) {
let totalCost = 0;
let costFieldsAll = parent.querySelectorAll(".costField");
[].forEach.call(costFieldsAll, (el) => {
totalCost += +el.innerText.slice(1);
});
hTotal.innerText = `Общая сумма: $${totalCost}`;
};
async function get1goodFromDB(_id) {
let result = await gql(
`query oneGood ($_id:String){
GoodFind(query:$_id ) {
_id
name
price
images {
url
}
}
}`,
{ _id: JSON.stringify([{ _id: _id }]) }
);
if (result.errors) {
console.log("не смог купить:", result.errors);
return;
}
return result.data.GoodFind[0];
}
async function show1goodFromBasket(parent, _id, count) {
let item = await get1goodFromDB(_id);
let div1 = document.createElement("div");
parent.append(div1);
let div = document.createElement("div");
div1.append(div);
let img = document.createElement("img");
div.append(img);
img.src = urlConst + `/` + item.images[0].url;
let p = document.createElement("p");
div.append(p);
p.append(item.name);
p = document.createElement("p");
div.append(p);
p.append(`Цена: $${item.price}`);
let inpCount = document.createElement("input");
inpCount.setAttribute("type", "number");
inpCount.setAttribute("min", "1");
inpCount.value = count;
div.append(inpCount);
let cost = document.createElement("p");
cost.setAttribute("class", "costField");
div.append(cost);
cost.append(`$${Math.round(count * item.price)}`);
let btn = appendActionBtn(div, { onTop: false }, "Удалить");
inpCount.oninput = async function () {
basketObj[loginId][_id] = +inpCount.value;
await updateBasketObj();
cost.innerHTML = `$${Math.round(inpCount.value * item.price)}`;
CountTotal(parent);
};
btn.onclick = async function () {
delete basketObj[loginId][_id];
if (!Object.keys(basketObj[loginId]).length) {
buyBtn.setAttribute("disabled", "disabled");
}
await updateBasketObj();
// div.remove();
CountTotal(parent);
};
// addEventListener();
btn.onclick = async function () {
delete basketObj[loginId][_id];
if (!Object.keys(basketObj[loginId]).length) {
buyBtn.setAttribute("disabled", "disabled");
}
await updateBasketObj();
div.remove();
CountTotal(parent);
};
}
async function buy() {
let order1 = { orderGoods: [] };
for (let [_id, count] of Object.entries(basketObj[loginId])) {
order1.orderGoods.push({
count: count,
good: { _id: _id },
});
}
let result = await gql(
`mutation newOrder1($order1:OrderInput) {
OrderUpsert(order: $order1) {
_id
total
}
}`,
{ order1 }
);
if (result.errors) {
console.log("не смог получить товар из DB для отображения в корзине:", result.errors);
return;
}
console.log(result.data.OrderUpsert.total);
alert(`Ваш заказ успешно оформлен. \nОбщая сумма заказа $${result.data.OrderUpsert.total}`);
for (let key in basketObj[loginId]) {
delete basketObj[loginId][key];
}
}
window.onload = init;