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 jwtDecode(token) { try { return JSON.parse(atob(token.split('.')[1])) } catch (e) { } } function authReducer(state = {}, { type, token }) { //{ // token, payload //} if (type === 'AUTH_LOGIN') { //пытаемся токен раскодировать const payload = jwtDecode(token) if (payload) { //и если получилось return { token, payload //payload - раскодированный токен; } } } if (type === 'AUTH_LOGOUT') { return {} } return state; } function countReducer(state = { count: 0 }, { type }) { if (type === "COUNT_INC") { return { count: state.count + 1 } } if (type === "COUNT_DEC") { return { count: state.count - 1 } } return state } function localStoreReducer(reducer, localStorageKey) { function localStoredReducer(state, action) { // Если state === undefined, то достать старый state из local storage if (state === undefined) { try { return JSON.parse(localStorage[localStorageKey]) } catch (e) { } } const newState = reducer(state, action) // Сохранить newState в local storage localStorage[localStorageKey] = JSON.stringify(newState) return newState } return localStoredReducer } function promiseReducer(state = {}, { type, name, status, payload, error }) { ////????? //ОДИН ПРОМИС: //состояние: PENDING/FULFILLED/REJECTED //результат //ошибка: //{status, payload, error} //{ // name1:{status, payload, error} // name2:{status, payload, error} // name3:{status, payload, error} //} if (type === 'PROMISE') { return { ...state, [name]: { status, payload, error } } } return state } const actionPending = (name) => ({ type: 'PROMISE', status: 'PENDING', name }) const actionFulfilled = (name, payload) => ({ type: 'PROMISE', status: 'FULFILLED', name, payload }) const actionRejected = (name, error) => ({ type: 'PROMISE', status: 'REJECTED', name, error }) const actionPromise = (name, promise) => async dispatch => { try { dispatch(actionPending(name)) let payload = await promise dispatch(actionFulfilled(name, payload)) return payload } catch (e) { dispatch(actionRejected(name, e)) } } const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms)) function combineReducers(reducers) { //пачку редьюсеров как объект {auth: authReducer, promise: promiseReducer} function combinedReducer(combinedState = {}, action) { //combinedState - типа {auth: {...}, promise: {....}} const newCombinedState = {} for (const [reducerName, reducer] of Object.entries(reducers)) { const newSubState = reducer(combinedState[reducerName], action) if (newSubState !== combinedState[reducerName]) { newCombinedState[reducerName] = newSubState } } if (Object.keys(newCombinedState).length === 0) { return combinedState } return { ...combinedState, ...newCombinedState } } return combinedReducer //нам возвращают один редьюсер, который имеет стейт вида {auth: {...стейт authReducer-а}, promise: {...стейт promiseReducer-а}} } function cartReducer(state = {}, { type, count = 1, good }) { // type CART_ADD CART_REMOVE CART_CLEAR CART_DEL // { // id1: {count: 1, good: {name, price, images, id}} // } if (type === "CART_ADD") { return { ...state, [good._id]: { count: count + (state[good._id]?.count || 0), good }, }; } if (type === "CART_DELETE") { if (state[good._id].count > 1) { return { ...state, [good._id]: { count: -count + (state[good._id]?.count || 0), good, }, }; } if (state[good._id].count === 1) { let { [good._id]: id1, ...newState } = state; //o4en strashnoe koldunstvo //delete newState[good._id] return newState; } } if (type === "CART_CLEAR") { return {}; } if (type === "CART_REMOVE") { // let newState = {...state} let { [good._id]: id1, ...newState } = state; //o4en strashnoe koldunstvo //delete newState[good._id] return newState; } return state; } const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/' //store.dispatch(actionPromise('delay1000', delay(1000))) //store.dispatch(actionPromise('delay3000', delay(3000))) //store.dispatch(actionPending('delay1000')) //delay(1000).then(result => store.dispatch(actionFulfilled('delay1000', result)), //error => store.dispatch(actionRejected('delay1000', error))) //store.dispatch(actionPending('delay3000')) //delay(3000).then(result => store.dispatch(actionFulfilled('delay3000', result)), //error => store.dispatch(actionRejected('delay3000', error))) 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"); // const gql = (url, query, variables) => fetch(url, { // method: 'POST', // headers: { // "Content-Type": "application/json", // Accept: "application/json", // }, // body: JSON.stringify({ query, variables }) // }).then(res => res.json()) // const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/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 } } } }`, { q: JSON.stringify([{ _id }]) } ) ) const actionGoodById = (_id) => actionPromise( 'goodByID', gql( `query goodByID($q:String){ GoodFindOne(query: $q){ _id name description price categories{ _id name } images{ url } } }`, { q: JSON.stringify([{ _id }]) } ) ) const actionRegistr = (login, password) => actionPromise( 'registr', gql( `mutation register($login:String, $password:String){ UserUpsert(user: {login:$login, password:$password}){ _id login } }`, { login: login, password: password } ) ) const actionLogin = (login, password) => actionPromise( 'login', gql( `query log($login:String, $password:String){ login(login:$login, password:$password) }`, { login: login, password: password } ) ) 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(actionCartClear()); document.location.hash = "#/cart/"; alert("Покупка успішна") } }; const orderHistory = () => actionPromise( "history", gql(` query OrderFind{ OrderFind(query:"[{}]"){ _id total createdAt orderGoods{ count good{ _id name price images{ url } } owner{ _id login } } } } `) ); const actionAuthLogin = (token) => (dispatch, getState) => { const oldState = getState() dispatch({ type: 'AUTH_LOGIN', token }) const newState = getState() if (oldState !== newState) localStorage.authToken = token } const actionAuthLogout = () => dispatch => { dispatch({ type: 'AUTH_LOGOUT' }) localStorage.removeItem('authToken') } const actionCartAdd = (good, count = 1) => ({ type: "CART_ADD", good, count }); const actionCartChange = (good, count = 1) => ({ type: "CART_CHANGE", good, count, }); ///oninput меняяем полностью const actionCartDelete = (good) => ({ type: "CART_DELETE", good }); const actionCartClear = () => ({ type: "CART_CLEAR" }); const actionFullLogin = (login, password) => async (dispatch) => { let token = await dispatch(actionLogin(login, password)) if (token) { dispatch(actionAuthLogin(token)) } } const actionFullRegistr = (login, password) => async (dispatch) => { try { await dispatch(actionRegistr(login, password)) } catch (e) { return console.log(e) } await dispatch(actionFullLogin(login, password)) } const store = createStore(combineReducers({ auth: authReducer, promise: promiseReducer, cart: localStoreReducer(cartReducer, "cart") })) //не забудьте combineReducers если он у вас уже есть if (localStorage.authToken) { store.dispatch(actionAuthLogin(localStorage.authToken)) } //const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer})) store.subscribe(() => console.log(store.getState())) store.dispatch(actionRootCats()) // store.dispatch(actionLogin('test456', '123123')) store.subscribe(() => { const { rootCats } = store.getState().promise if (rootCats?.payload) { aside.innerHTML = '' for (let { _id, name } of rootCats?.payload) { const a = document.createElement('a') a.href = `#/category/${_id}` a.innerHTML = name aside.append(a) } } }) store.subscribe(() => { const { catById } = store.getState().promise const [, route, _id] = location.hash.split('/') if (catById?.payload && route === 'category') { const { name, goods, _id } = catById?.payload main.innerHTML = `

${name}

` for (let { _id, name, price, images } of goods) { const cards = document.createElement("div") cards.innerHTML = `

${name}

Ціна ${price} грн Перейти `; main.append(cards); // const a = document.createElement('a') // const p = document.createElement('p') // p.href = `#/good/${_id}` // a.href = `#/good/${_id}` // a.innerHTML = name // p.innerHTML = price // main.append(a) // main.append(p) } } }) store.subscribe(() => { const { goodByID } = store.getState().promise const [, route, _id] = location.hash.split('/') if (goodByID?.payload && route === 'good') { main.innerHTML = "" const { name, images, price, description } = goodByID?.payload // main.innerHTML = `

${name}

Продукт` const cards = document.createElement("div") cards.innerHTML = `

${name}

Ціна ${price} грн

${description}

`; main.append(cards) cards.style.marginTop = "10px" var btn = document.getElementById('buy') btn.onclick = () => { store.dispatch(actionCartAdd(goodByID.payload)) } } }) const bPopupContent = document.createElement("div"); const obertkaDlyaTovara = document.createElement("div") const all = document.createElement('h2') const checkout = document.createElement("button") const clearToCart = document.createElement("button") store.subscribe(() => { obertkaDlyaTovara.innerHTML = "" const cartById = store.getState().cart let productCount = 0; let productPrice = 0 for (let gPC of Object.values(cartById)) { const { good,count } = gPC productCount += count productPrice += good.price * count const tovar = document.createElement("div") tovar.id = "tovar" tovar.style.border = "3px solid blue" tovar.style.marginTop = "10px" const name = document.createElement('h1') const price = document.createElement('h3') const countById = document.createElement('p') const divDlyaKnopok = document.createElement("div") const plus = document.createElement("button") const minus = document.createElement("button") plus.innerText = "+" minus.innerText = "-" tovar.append(name) tovar.append(price) tovar.append(countById) divDlyaKnopok.append(plus) divDlyaKnopok.append(minus) tovar.append(divDlyaKnopok) name.innerHTML = good.name price.innerHTML = good.price countById.innerHTML = count obertkaDlyaTovara.append(tovar) bPopupContent.append(obertkaDlyaTovara) plus.onclick = () => { store.dispatch(actionCartAdd(good)) } minus.onclick = () => { store.dispatch(actionCartDelete(good)) } } clearToCart.id = "clearToCart" clearToCart.innerHTML = "Очистити кошик" clearToCart.style.margin = "0 auto" clearToCart.style.marginBottom = "20px" clearToCart.style.background = "blue" clearToCart.style.color = "yellow" bPopupContent.append(clearToCart) checkout.id = "checkout" checkout.innerHTML = "Оформити замовлення" checkout.style.margin = "0 auto" checkout.style.background = "blue" checkout.style.color = "yellow" bPopupContent.append(checkout) all.id = "all" all.innerHTML = "Всього: " + productPrice bPopupContent.append(all) all.style.marginLeft = "90%" clearToCart.onclick = () => { all.innerHTML = " " store.dispatch(actionCartClear()) } checkout.onclick = () => { all.innerHTML = " " store.dispatch(actionOrder()); store.dispatch(orderHistory()); } }) store.subscribe(() => { batton.onclick = () => { store.dispatch(actionFullLogin(login.value, password.value)) } battonchik.onclick = () => { store.dispatch(actionFullRegistr(login.value, password.value)) } battonSMakom.onclick = () => { store.dispatch(actionAuthLogout()) } const payload = store.getState().auth.token; if (payload) { korzina.style.display = "block" login.style.display = "none" password.style.display = "none" batton.style.display = "none" battonchik.style.display = "none" battonSMakom.style.display = "block" accaunt.style.display = "block" accaunt.innerText = jwtDecode(payload).sub.login; purchaseHistory.style.display = "block" } else { korzina.style.display = "none" battonSMakom.style.display = "none" login.style.display = "block" password.style.display = "block" batton.style.display = "block" battonchik.style.display = "block" accaunt.style.display = "none" purchaseHistory.style.display = "none" } }) store.dispatch(orderHistory()); const h2 = document.createElement("h2") store.subscribe(() => { const { history } = store.getState().promise; const [, route] = location.hash.split("/"); purchaseHistory.onclick = () => { const bPopup = document.createElement("div"); const bPopupContent = document.createElement("div"); bPopup.id = "b-popup"; bPopup.className = "b-popup"; bPopupContent.className = "b-popup-content b-poput-container-flex"; header.append(bPopup); bPopup.append(bPopupContent); const buttonCloseCart = document.createElement("button"); buttonCloseCart.innerText = "×"; buttonCloseCart.id = "buttonCloseCartId"; bPopupContent.append(buttonCloseCart); buttonCloseCart.onclick = () => { var parent = document.getElementById("header"); var child = document.getElementById("b-popup"); parent.removeChild(child); }; for (let [key, value] of Object.entries(history.payload)) { const { _id, createdAt, total, orderGoods } = value; const h2 = document.createElement("h2"); h2.className = "h2History" const dateOfOrder = new Date(+createdAt); h2.innerHTML = `${dateOfOrder.toLocaleDateString()} ${dateOfOrder.toLocaleTimeString()} Order ID: ${_id} от , c ${orderGoods.length} goods worth: ${total}`; bPopupContent.append(h2); } if (Object.keys(history.payload).length == 0) { const p = document.createElement("p"); p.innerHTML = "

Ще немає покупок

"; card.append(p); } }; }); const buttonCloseCart = document.createElement("button"); buttonCloseCart.innerText = `×`; buttonCloseCart.id = "buttonCloseCartId"; bPopupContent.append(buttonCloseCart); buttonCloseCart.onclick = () => { var parent = document.getElementById("header"); var child = document.getElementById("b-popup"); parent.removeChild(child); }; function bPopupCreate() { const bPopup = document.createElement("div"); bPopup.id = "b-popup"; bPopup.className = "b-popup"; bPopupContent.className = "b-popup-content b-poput-container-flex"; header.append(bPopup); bPopup.append(bPopupContent); } korzina.onclick = () => { bPopupCreate() } window.onhashchange = () => { const [, route, _id] = location.hash.split('/') console.log(route, _id) const routes = { category() { store.dispatch(actionCatById(_id)) }, good() { store.dispatch(actionGoodById(_id)) }, dashboard() { store.dispatch(orderHistory()); }, } if (route in routes) { routes[route]() } } window.onhashchange()