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 = "
${price}$
${description}