function createStore(reducer) { let state = reducer(undefined, {}); //стартовая инициализация состояния, запуск редьюсера со state === undefined let cbs = []; //массив подписчиков const getState = () => state; //функция, возвращающая переменную из замыкания const subscribe = (cb) => ( cbs.push(cb), //запоминаем подписчиков в массиве () => (cbs = cbs.filter((c) => c !== cb)) ); //возвращаем функцию unsubscribe, которая удаляет подписчика из списка const dispatch = (action) => { if (typeof action === "function") { //если action - не объект, а функция return action(dispatch, getState); //запускаем эту функцию и даем ей dispatch и getState для работы } const newState = reducer(state, action); //пробуем запустить редьюсер if (newState !== state) { //проверяем, смог ли редьюсер обработать action state = newState; //если смог, то обновляем state for (let cb of cbs) cb(); //и запускаем подписчиков } }; return { getState, //добавление функции getState в результирующий объект dispatch, subscribe, //добавление subscribe в объект }; } function authReducer(state, { type, token }) { if (!state) { if (localStorage.authToken) { type = "AUTH_LOGIN"; token = localStorage.authToken; } else { return {}; } } if (type === "AUTH_LOGIN") { localStorage.authToken = token; let payload = jwtDecode(token); if (typeof payload !== "object") { return {}; } return { token, payload }; } if (type === "AUTH_LOGOUT") { localStorage.authToken = ""; return {}; } return state; } const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token, }); const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" }); function cartReducer(state = {}, { type, good = {}, count }) { const { _id } = good; const types = { CART_ADD() { count = +count; if (!count) { return state; } return { ...state, [_id]: { good, count: count + (state[_id]?.count || 0) }, }; }, CART_CHANGE() { count = +count; if (!count) { return state; } return { ...state, [_id]: { good, count }, }; }, CART_REMOVE() { let { [_id]: remove, ...goods } = state; return goods; }, CART_CLEAR() { return {}; }, CART_SHOW() { state = JSON.parse(localStorage.cart); return state; }, }; if (type in types) { return types[type](); } return state; } function promiseReducer(state = {}, { type, status, payload, errors, name }) { if (!state) { return {}; } if (type === "PROMISE") { return { ...state, [name]: { status, payload, errors }, }; } return state; } const actionPending = (name) => ({ type: "PROMISE", status: "PENDING", name }); const actionResolved = (name, payload) => ({ type: "PROMISE", status: "RESOLVED", name, payload, }); const actionRejected = (name, errors) => ({ type: "PROMISE", status: "REJECTED", name, errors, }); const actionPromise = (name, promise) => async (dispatch) => { dispatch(actionPending(name)); try { let data = await promise; dispatch(actionResolved(name, data)); return data; } catch (error) { dispatch(actionRejected(name, error)); } }; function combineReducers(reducers) { return (state = {}, action) => { const newState = {}; let newSubState; for (const [reducerName, reducer] of Object.entries(reducers)) { newSubState = reducer(state[reducerName], action); if (state[reducerName] !== newSubState) { newState[reducerName] = newSubState; } } if (Object.keys(newState).length !== 0) { return { ...state, ...newState }; } else { return state; } }; } const combinedReducer = combineReducers({ promise: promiseReducer, auth: authReducer, cart: cartReducer, }); const store = createStore(combinedReducer); const actionCartAdd = (good, count) => ({ type: "CART_ADD", good, count, }); const actionCartChange = (good, count) => ({ type: "CART_CHANGE", good, count, }); const actionCartRemove = (good) => ({ type: "CART_REMOVE", good }); const actionCartClear = () => ({ type: "CART_CLEAR" }); const actionCartShow = () => ({ type: "CART_SHOW" }); const actionOrder = () => async (dispatch, getState) => { let { cart } = getState(); const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({ good: { _id }, count, })); let result = await dispatch( actionPromise( "order", gql( ` mutation newOrder($order:OrderInput){ OrderUpsert(order:$order) { _id total } } `, { order: { orderGoods } } ) ) ); if (result?._id) { dispatch(actionCleanCart()); } }; const getGQL = (url) => async (query, variables = {}) => { let obj = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ query, variables }), }); let a = await obj.json(); if (!a.data && a.errors) throw new Error(JSON.stringify(a.errors)); return a.data[Object.keys(a.data)[0]]; }; const backURL = "http://shop-roles.asmer.fs.a-level.com.ua"; const gql = getGQL(backURL + "/graphql"); const actionRootCats = () => actionPromise( "rootCats", gql(`query { CategoryFind(query: "[{\\"parent\\":null}]"){ _id name } }`) ); const actionCatById = (_id) => actionPromise( "catById", gql( `query catById($q: String){ CategoryFindOne(query: $q){ _id name, goods{ _id name price images { url } }, subCategories{ name, subCategories{ name } } } }`, { q: JSON.stringify([{ _id }]) } ) ); const actionGoodById = (_id) => actionPromise( "goodById", gql( `query goodById($q: String){ GoodFindOne(query: $q){ _id name description price images{ url } } }`, { q: JSON.stringify([{ _id }]) } ) ); const actionLogin = (login, password) => actionPromise( "login", gql( `query log($login: String, $password: String) { login(login: $login, password: $password) }`, { login: login, password: password } ) ); const actionFullLogin = (login, password) => async (dispatch) => { console.log(login, password); let token = await dispatch(actionLogin(login, password)); console.log(token); if (token) { dispatch(actionAuthLogin(token)); } }; const actionRegister = (login, password) => actionPromise( "registration", gql( `mutation reg2($user:UserInput) { UserUpsert(user:$user) { _id login } } `, { user: { login: login, password: password } } ) ); const actionFullRegister = (login, password) => async (dispatch) => { console.log(login, password); let check = await dispatch(actionRegister(login, password)); console.log(check); if (check) { dispatch(actionFullLogin(login, password)); } }; store.dispatch(actionRootCats()); store.dispatch(actionGoodById()); store.subscribe(() => { const { promise } = store.getState(); if (promise?.rootCats?.payload) { asideList.innerHTML = ""; let count = -1; for (const { _id, name } of promise.rootCats.payload) { count++; const link = document.createElement("a"); link.href = `#/category/${_id}`; link.innerText = name; link.className = "nav-link"; link.id = `rootCat${count}`; asideList.appendChild(link); } } }); const openCart = () => { const [, route] = location.hash.split("/"); if (route === "cart") { main.innerHTML = ""; const { cart } = store.getState(); for (let good in cart) { let { good: { _id: id, name: name, price: price, images: [{ url }], }, count, } = cart[good]; let cardMain = document.createElement("div"); cardMain.id = "cardMain"; let goodImgBlock = document.createElement("div"); goodImgBlock.innerHTML = ` `; cardMain.appendChild(goodImgBlock); let goodInfoBlock = document.createElement("div"); let goodName = document.createElement("h2"); goodName.innerText = `${name}`; let goodPrice = document.createElement("p"); goodPrice.innerText = `${price * count}$`; let goodCount = document.createElement("p"); goodCount.innerText = `${count} шт.`; goodInfoBlock.appendChild(goodName); goodInfoBlock.appendChild(goodPrice); goodInfoBlock.appendChild(goodCount); cardMain.appendChild(goodInfoBlock); let br = document.createElement("br"); let goodEditBlock = document.createElement("div"); goodEditBlock.className = "form-outline"; let deleteBtn = document.createElement("button"); deleteBtn.innerText = "Delete"; deleteBtn.id = "deleteBtn"; deleteBtn.type = "button"; deleteBtn.className = "btn btn-danger"; goodEditBlock.appendChild(deleteBtn); goodEditBlock.appendChild(br); deleteBtn.onclick = () => { store.dispatch(actionCartRemove(cart[good].good)); cardMain.remove(); console.log(store.getState()); }; let countLabel = document.createElement("label"); countLabel.className = "form-label"; countLabel.htmlFor = "countField"; countLabel.innerText = "Change count"; let countField = document.createElement("input"); countField.type = "number"; countField.value = cart[good].count; countField.min = "1"; countField.id = "countField"; countField.className = "form-control"; goodEditBlock.appendChild(countLabel); goodEditBlock.appendChild(countField); countField.oninput = () => { goodPrice.innerText = `${price * +countField.value}$`; goodCount.innerText = `${countField.value} шт.`; store.dispatch(actionCartChange(cart[good].good, countField.value)); }; cardMain.appendChild(goodEditBlock); main.appendChild(cardMain); } let clearBtn = document.createElement("button"); clearBtn.innerText = "Clear Cart"; clearBtn.id = "clearBtn"; clearBtn.type = "button"; clearBtn.className = "btn btn-dark"; let makeOrder = document.createElement("button"); makeOrder.innerText = "Make Order"; makeOrder.id = "makeOrder"; makeOrder.type = "button"; makeOrder.className = "btn btn-success"; if (Object.keys(cart).length !== 0) { main.appendChild(makeOrder); main.appendChild(clearBtn); } else { main.innerHTML = "

Your cart is empty!

"; } makeOrder.onclick = () => { store.dispatch(actionOrder()); alert("Thanks for your order!"); store.dispatch(actionCartClear()); document.location.reload(); }; clearBtn.onclick = () => { store.dispatch(actionCartClear()); document.location.reload(); }; } }; window.onhashchange = () => { const [, route, _id] = location.hash.split("/"); let signOut = document.createElement("button"); signOut.innerText = "Sign Out"; signOut.id = "signoutBtn"; signOut.className = "btn btn-secondary"; const routes = { category() { store.dispatch(actionCatById(_id)); }, good() { store.dispatch(actionGoodById(_id)); }, login() { let loginInput = document.getElementById("loginInput"); let passwordInput = document.getElementById("pass1Input"); if ( loginInput && passwordInput && loginInput.value && passwordInput.value ) { store.dispatch( actionFullLogin( loginInput.value.slice(0, loginInput.value.indexOf("@")), passwordInput.value ) ); } else { throw new Error("Error login"); } }, register() { let regInput = document.getElementById("regInput"); let passwordInput = document.getElementById("pass2Input"); if (regInput && passwordInput && regInput.value && passwordInput.value) { store.dispatch( actionFullRegister( regInput.value.slice(0, regInput.value.indexOf("@")), passwordInput.value ) ); } else { throw new Error("Error register"); } }, cart() { openCart(); }, }; if (route in routes) routes[route](); }; window.onhashchange(); store.subscribe(() => { const [, route] = location.hash.split("/"); const { cart } = store.getState(); if (Object.keys(cart).length === 0 && route === "cart") { let makeOrderBtn = document.getElementById("makeOrder"); let clearCartBtn = document.getElementById("clearBtn"); if (makeOrderBtn) { makeOrderBtn.remove(); } if (clearCartBtn) { clearCartBtn.remove(); } localStorage.cart = ""; main.innerHTML = "

Your cart is empty!

"; } else if (Object.keys(cart).length !== 0) { let cartStr = JSON.stringify(cart); localStorage.cart = cartStr; } }); window.onunload = () => { if (localStorage.cart !== "") { store.dispatch(actionCartShow()); openCart(); } }; window.onunload(); store.subscribe(() => { const { promise } = store.getState(); const [, route, _id] = location.hash.split("/"); if (promise?.catById?.payload && route === "category") { const { name } = promise.catById.payload; main.innerHTML = `

${name}

`; if (promise.catById.payload?.subCategories) { for (const { _id, name } of promise.catById.payload.subCategories) { const rootCat = document.querySelector( `a[href="#/category/${promise.catById.payload._id}"]` ); const subNav = document.createElement("nav"); subNav.className = "nav nav-pills flex-column"; const link = document.createElement("a"); link.href = `#/category/${_id}`; link.innerText = name; link.className = "nav-link ms-3 my-1"; subNav.appendChild(link); rootCat.appendChild(subNav); } } if (promise.catById.payload?.goods) { for (const good of promise.catById.payload.goods) { const { _id, name, price, images } = good; const card = document.createElement("div"); const link = document.createElement("a"); card.innerHTML = `

${name}


${price}$
`; link.innerText = name; link.href = `#/good/${_id}`; card.appendChild(link); let buyBtn = document.createElement("button"); buyBtn.className = "btn btn-success"; buyBtn.textContent = "Buy"; buyBtn.onclick = () => { store.dispatch(actionCartAdd(good, 1)); }; card.appendChild(buyBtn); main.append(card); } } } }); store.subscribe(() => { const { cart } = store.getState(); let countGoods = 0; for (let good of Object.keys(cart)) { countGoods += cart[good].count; } cartIcon.innerText = `Товаров в корзине: ${countGoods}`; cartIcon.onclick = () => { window.location.href = "#/cart"; }; }); store.subscribe(() => { const { promise } = store.getState(); const [, route, _id] = location.hash.split("/"); if (promise?.goodById?.payload && route === "good") { main.innerHTML = ""; const { name, description, price, images } = promise.goodById.payload; main.innerHTML = `

${name}

${price}$

${description}

`; } }); store.subscribe(() => console.log(store.getState())); function jwtDecode(token) { try { return JSON.parse(atob(token.split(".")[1])); } catch (error) { console.log(error); } } store.subscribe(async () => { const { promise } = store.getState(); let signinBtn = document.getElementById("signinBtn"); let signupBtn = document.getElementById("signupBtn"); if (promise?.rootCats?.payload && !signinBtn && !signupBtn) { let signIn = document.createElement("button"); signIn.innerText = "Sign In"; signIn.id = "signinBtn"; signIn.type = "button"; signIn.className = "btn btn-light"; let signUp = document.createElement("button"); signUp.innerText = "Sign Up"; signUp.id = "signupBtn"; signUp.type = "button"; signUp.className = "btn btn-light"; let authBlock = document.createElement("div"); authBlock.id = "authBlock"; authBlock.appendChild(signIn); authBlock.appendChild(signUp); authnav.appendChild(authBlock); } }); store.subscribe(async () => { let signinBtn = document.getElementById("signinBtn"); if (signinBtn) { signinBtn.onclick = () => { try { let registerFields = document.getElementById("registerFields"); if (registerFields !== null) { registerFields.style.display = "none"; } signinBtn.style.display = "none"; signupBtn.style.display = "inline"; let fieldsBlock = document.createElement("div"); fieldsBlock.className = "fields"; fieldsBlock.id = "loginFields"; let email = document.createElement("input"); email.type = "email"; email.placeholder = "Enter your email"; email.className = "form-control"; email.id = "loginInput"; let password = document.createElement("input"); password.type = "password"; password.placeholder = "Enter your password"; password.className = "form-control"; password.id = "pass1Input"; let login = document.createElement("button"); login.innerText = "Login"; login.id = "loginBtn"; login.className = "btn btn-primary"; fieldsBlock.appendChild(email); fieldsBlock.appendChild(password); fieldsBlock.appendChild(login); authBlock.prepend(fieldsBlock); loginBtn.onclick = () => { window.location.href = "#/login"; console.log(store.getState()); }; } catch (error) { console.log(error); } }; } }); store.subscribe(async () => { let signupBtn = document.getElementById("signupBtn"); if (signupBtn) { signupBtn.onclick = () => { try { let loginFields = document.getElementById("loginFields"); if (loginFields !== null) { loginFields.style.display = "none"; } signinBtn.style.display = "inline"; signupBtn.style.display = "none"; let fieldsBlock = document.createElement("div"); fieldsBlock.className = "fields"; fieldsBlock.id = "registerFields"; let email = document.createElement("input"); email.type = "email"; email.placeholder = "Enter your email"; email.className = "form-control"; email.id = "regInput"; let password = document.createElement("input"); password.type = "password"; password.placeholder = "Enter your password"; password.className = "form-control"; password.id = "pass2Input"; let register = document.createElement("button"); register.innerText = "Register"; register.id = "regBtn"; register.className = "btn btn-primary"; fieldsBlock.appendChild(email); fieldsBlock.appendChild(password); fieldsBlock.appendChild(register); authBlock.prepend(fieldsBlock); regBtn.onclick = () => { window.location.href = "#/register"; console.log(store.getState()); }; } catch (error) { console.log(error); } }; } }); store.subscribe(async () => { const { promise, auth } = store.getState(); let accountLink = document.getElementById("accountLink"); let signoutBtn = document.getElementById("signoutBtn"); let signinBtn = document.getElementById("signinBtn"); let signupBtn = document.getElementById("signupBtn"); if (auth?.token && !signoutBtn && !accountLink) { signinBtn.style.display = "none"; signupBtn.style.display = "none"; if (promise?.register?.status === "RESOLVED") { let registerBlock = document.getElementById("registerFields"); registerBlock.style.display = "none"; signupBtn.style.display = "none"; window.location.href = window.location.href.replace("#/register", ""); console.log(store.getState()); } if (promise?.login?.status === "RESOLVED") { let loginBlock = document.getElementById("loginFields"); loginBlock.style.display = "none"; signinBtn.style.display = "none"; window.location.href = window.location.href.replace("#/login", ""); console.log(store.getState()); } let signOut = document.createElement("button"); signOut.innerText = "Sign Out"; signOut.id = "signoutBtn"; signOut.className = "btn btn-secondary"; let authBlock = document.createElement("div"); authBlock.id = "authBlock"; authBlock.appendChild(signOut); let nickname = auth.payload.sub.login; let account = document.createElement("button"); account.textContent = nickname; account.type = "button"; account.id = "accountLink"; account.className = "btn btn-success"; authBlock.prepend(account); authnav.appendChild(authBlock); signOut.onclick = () => { signOut.style.display = "none"; store.dispatch(actionAuthLogout()); signinBtn.style.display = "inline"; signupBtn.style.display = "inline"; account.remove(); }; } });