cartIcon.addEventListener("click", () => { document.querySelector("#cartList").classList.remove("hide"); }); const backendURL = "http://shop-roles.asmer.fs.a-level.com.ua"; //store 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") { 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 в объект }; } const getGQL = (url) => (query, variables) => fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...(localStorage.authToken ? { Authorization: "Bearer " + localStorage.authToken } : {}), }, body: JSON.stringify({ query, variables }), }) .then((res) => res.json()) .then((data) => { if (data.data) { return Object.values(data.data)[0]; } else throw new Error(JSON.stringify(data.errors)); }); const gql = getGQL(backendURL + "/graphql"); function promiseReducer(state = {}, { type, name, status, payload, error }) { if (type === "PROMISE") { return { ...state, //.......скопировать старый state [name]: { status, payload, error }, //....... перекрыть в нем один name на новый объект со status, payload и error }; } return state; } const actionPromise = (name, promise) => async (dispatch) => { dispatch(actionPending(name)); try { let payload = await promise; dispatch(actionFulfilled(name, payload)); return payload; } catch (error) { dispatch(actionRejected(name, error)); } }; //actions const actionPending = (name) => ({ type: "PROMISE", name, status: "PENDING" }); const actionFulfilled = (name, payload) => ({ type: "PROMISE", name, status: "FULFILLED", payload, }); const actionRejected = (name, error) => ({ type: "PROMISE", name, status: "REJECTED", error, }); 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 } } } }`, { q: JSON.stringify([{ _id }]) } ) ); const actionGoodById = (_id) => actionPromise( "goodById", gql( `query goodById($q: String){ GoodFindOne(query:$q){ _id name price description categories{_id name} images{url} } } `, { q: JSON.stringify([{ _id }]) } ) ); const actionLogin = (login, password) => actionPromise( "login", gql( `query login($login: String, $password: String){ login(login: $login, password: $password) }`, { login: login, password: password } ) ); const actionRegister = (login, password) => actionPromise( "register", gql( `mutation register($login:String, $password:String) { UserUpsert(user: {login:$login, password: $password}) { _id login } }`, { login: login, password: password } ) ); const actionAddToCard = (good, amount = 1) => ({ type: "ADD_TO_CARD", good, amount, }); const actionChangeAmount = (good, amount = 1) => ({ type: "CHANGE_AMOUNT", good, amount, }); const actionRemoveFromCard = (good, amount = 1) => ({ type: "REMOVE_FROM_CARD", good, amount, }); const actionRemoveCard = () => ({ type: "REMOVE_CARD" }); const actionFullLogin = (login, password) => async (dispatch) => { let token = await dispatch(actionLogin(login, password)); if (token) { dispatch(actionAuthLogin(token)); } }; const actionFullRegister = (login, password) => async (dispatch) => { try { await dispatch(actionRegister(login, password)); } catch (error) { return console.log(error); } await dispatch(actionFullLogin(login, password)); }; const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token }); const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" }); const jwtDecode = (token) => { try { const payload = JSON.parse(atob(token.split(".")[1])); return payload; } catch (e) {} }; //reducers function authReducer(state, { type, token }) { if (state === undefined && localStorage.authToken) { token = localStorage.authToken; type = "AUTH_LOGIN"; } if (type === "AUTH_LOGIN") { // раскодируем токен let decode = jwtDecode(token); // (пишем отдельно функцию jwtDecode, и да будет в ней try-catch) if (decode) { // серединка, atob, JSON.parse localStorage.authToken = token; //если получилось пишем его в localStorage return { token, payload: decode }; // return{token, payload} } } if (type === "AUTH_LOGOUT") { localStorage.removeItem("authToken"); //чистим localStorage.authToken return {}; //возвращаем {} } return state || {}; } function cartReducer(state = {}, { type, good = {}, amount = 1 }) { const id = good._id; const types = { ADD_TO_CARD() { return { ...state, [id]: { good, count: +amount + +(state[id] ? +state[id].count : 0) }, }; }, CHANGE_AMOUNT() { return { ...state, [id]: { good, count: +amount }, }; }, REMOVE_FROM_CARD() { let newState = { ...state }; delete newState[id]; return { ...newState, }; }, REMOVE_CARD() { return {}; }, }; if (type in types) return types[type](); return state; } function combineReducers(reducers) { return (state = {}, action) => { const newState = {}; for (const [nameOfReducer, currentReducer] of Object.entries(reducers)) { let newCurrentState = currentReducer(state[nameOfReducer], action); if (newCurrentState !== state[nameOfReducer]) { newState[nameOfReducer] = newCurrentState; } } return Object.keys(newState).length !== 0 ? { ...state, ...newState } : state; }; } const combinedReducer = combineReducers({ goods: promiseReducer, auth: authReducer, cart: cartReducer, }); const store = createStore(combinedReducer); store.dispatch(actionAuthLogout()); store.dispatch(actionRootCats()); //subscribes store.subscribe(() => console.log(store.getState())); store.subscribe(() => { const { rootCats } = store.getState().goods; if (rootCats?.payload) { aside.innerHTML = ""; for (const { _id, name } of rootCats.payload) { const link = document.createElement("a"); link.href = `#/category/${_id}`; link.innerText = name; aside.append(link); } } }); store.subscribe(() => { const { catById } = store.getState().goods; const [, route, _id] = location.hash.split("/"); if (catById?.payload && route === "category") { const { name } = catById.payload; main.innerHTML = `
${description}
`; main.append(card); document.querySelector(`.buy${_id}`).addEventListener("click", () => { store.dispatch(actionAddToCard(goodById.payload)); }); } }); store.subscribe(() => { const cartState = store.getState().cart; cartList.innerHTML = `- ${good.name}:
`; const inputCount = document.createElement("input"); inputCount.type = "number"; inputCount.value = count; inputCount.addEventListener("change", (e) => { store.dispatch(actionChangeAmount(good, e.target.value)); }); cartListItem.appendChild(inputCount); const goodPrice = document.createElement("p"); goodPrice.innerText = `цена: ${good.price}`; cartListItem.appendChild(goodPrice); const goodCost = document.createElement("strong"); goodCost.innerText = `всего ${+good.price * +count} грн.`; sum += +good.price * +count; cartListItem.appendChild(goodCost); const removeBtn = document.createElement("button"); removeBtn.innerText = "remove"; removeBtn.addEventListener("click", () => { store.dispatch(actionRemoveFromCard(good)); }); cartListItem.appendChild(removeBtn); cartList.appendChild(cartListItem); } if (cartList.querySelectorAll(".cartListItem").length) { const btnToOrder = document.createElement("button"); btnToOrder.innerText = "Заказать"; btnToOrder.classList = "btnToOrder"; cartList.appendChild(btnToOrder); btnToOrder.addEventListener("click", () => { // store.dispatch(actionToOrder()); console.log("Ваш заказ в процессе обработки"); }); const priceOfOrder = document.createElement("div"); priceOfOrder.classList = "priceOfOrder"; priceOfOrder.innerText = `Итого : ${sum}`; cartList.appendChild(priceOfOrder); const btnDeleteAll = document.createElement("button"); btnDeleteAll.classList = "btnDeleteAll"; btnDeleteAll.innerText = "Очистить всё"; btnDeleteAll.addEventListener("click", () => { store.dispatch(actionRemoveCard()); }); cartList.appendChild(btnDeleteAll); } }); window.onhashchange = () => { const [, route, _id] = location.hash.split("/"); const routes = { category() { store.dispatch(actionCatById(_id)); }, good() { //задиспатчить actionGoodById store.dispatch(actionGoodById(_id)); }, login() { loginBlock.classList.add("hide"); const innerContent = `Привет, ${login}
`; header.removeChild(loginWrapper); const btnLogout = document.createElement("button"); btnLogout.textContent = "Exit"; btnLogout.addEventListener("click", () => { userData.innerHTML = ""; store.dispatch(actionAuthLogout()); loginBlock.style.display = "block"; oders.classList.add("hide"); }); orders.classList.remove("hide"); userData.append(btnLogout); } }); }, register() { loginBlock.classList.add("hide"); const innerContent = `