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(state) //и запускаем подписчиков } } return { getState, //добавление функции getState в результирующий объект dispatch, subscribe //добавление subscribe в объект } } function combineReducers(reducers){ function totalReducer(state={}, action){ const newTotalState = {} for (const [reducerName, reducer] of Object.entries(reducers)){ const newSubState = reducer(state[reducerName], action) if (newSubState !== state[reducerName]){ newTotalState[reducerName] = newSubState } } if (Object.keys(newTotalState).length){ return {...state, ...newTotalState} } return state } return totalReducer } function jwtDecode(token){ try { return JSON.parse(atob(token.split('.')[1])) } catch(e){ } } function localStoredReducer(reducer, localStorageKey){ function wrapper(state, action){ if (state === undefined){ try { return JSON.parse(localStorage[localStorageKey]) } catch(e){ } } const newState = reducer(state, action) localStorage.setItem(localStorageKey, JSON.stringify(newState)) return newState } return wrapper } const reducers = { auth: authReducer, cart: localStoredReducer(cartReducer, 'cart'), promise: localStoredReducer(promiseReducer, 'promise'), } function promiseReducer(state={}, {type, status, payload, error,namePromise}){ if (type === 'PROMISE'){ return { ...state, [namePromise] : {status, payload, error} } } return state } const actionPending = (namePromise) => ({type: 'PROMISE', status: 'PENDING',namePromise}) const actionFulfilled = (namePromise,payload) => ({type: 'PROMISE', status: 'FULFILLED',namePromise, payload}) const actionRejected = (namePromise,error) => ({type: 'PROMISE', status: 'REJECTED', namePromise, error}) const actionPromise = (namePromise,promise) => async dispatch => { dispatch(actionPending(namePromise)) //сигнализируем redux, что промис начался try{ const payload = await promise //ожидаем промиса dispatch(actionFulfilled(namePromise,payload)) //сигнализируем redux, что промис успешно выполнен return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса } catch (error){ dispatch(actionRejected(namePromise,error)) //в случае ошибки - сигнализируем redux, что промис несложился } } const store = createStore(combineReducers(reducers)) //не забудьте combineReducers если он у вас уже есть store.subscribe(() => console.log(store.getState())) function authReducer(state={}, {type, token}){ if (type === 'AUTH_LOGIN'){ const payload = jwtDecode(token) if (payload){ return { token, payload } } } if (type === 'AUTH_LOGOUT'){ return {} } return state } const actionAuthLogout = () => () => { store.dispatch({type: 'AUTH_LOGOUT'}); localStorage.removeItem('authToken'); } const actionAuthLogin = (token) => () => { const oldState = store.getState() store.dispatch({type: 'AUTH_LOGIN', token}) const newState = store.getState() if (oldState !== newState) localStorage.setItem('authToken', token) } function cartReducer(state={}, {type, count, good}){ if(type==='CART_ADD'){ if(typeof state[good._id]==='object'){ let newCount = state[good._id].count+count return{ ...state, [good._id]:{good:good, count:newCount} } } else{return{ ...state, [good._id]:{good:good, count} } } } if(type==='CART_SET'){ return{ ...state, [good._id]:{good:good, count} } } if(type==='CART_SUB'){ let newCount = state[good._id].count-count if(newCount>0){ return{ ...state, [good._id]:{good:good, count:newCount} } }else{ delete state[good._id] } } if(type==='CART_DEL'){ const {[good._id]: x,...newState} = state return newState } if(type==='CART_CLEAR'){ return state={} } return state } const actionCartAdd = (good, count=1) => ({type: 'CART_ADD', count, good}) const actionCartSub = (good, count=1) => ({type: 'CART_SUB', count, good}) const actionCartDel = (good) => ({type: 'CART_DEL', good}) const actionCartSet = (good, count=1) => ({type: 'CART_SET', count, good}) const actionCartClear = () => ({type: 'CART_CLEAR'}) const checkToken = () => { const headers = { "Content-Type": "application/json", "Accept": "application/json", } if(localStorage.getItem('authToken')) { return { ...headers, "Authorization": `Bearer ${localStorage.getItem('authToken')}` } } else { return headers; } } const getGQL = url => (query, variables= {}) => fetch(url, { method: 'POST', headers: checkToken(), body:JSON.stringify({query, variables}) }).then(res => res.json()) .then(data => { try { if(!data.data && data.errors) { throw new SyntaxError(`SyntaxError - ${JSON.stringify(Object.values(data.errors)[0])}`); } return Object.values(data.data)[0]; } catch (e) { console.error(e); } }); const url = 'http://shop-roles.node.ed.asmer.org.ua/' const gql = getGQL(url + 'graphql') const rootCats = () => actionPromise('rootCats', gql(`query rootCats2{ CategoryFind(query: "[{\\"parent\\": null}]"){ _id name subCategories{_id name} } }`)) store.dispatch(rootCats()) const categoryGoods = (_id) => actionPromise('categoryGoods', gql(`query categoryGoods ($q:String) { CategoryFindOne(query: $q) { _id name parent { _id name } subCategories { _id name } goods { _id name price description images { url } } } }`, {q: JSON.stringify([{_id}])} )) const Img = (_id) => actionPromise('Img', gql(`query Img ($q:String) { GoodFindOne (query: $q){ _id name price description images { url } }}`, {q: JSON.stringify([{_id}])} )) const actionRegister = (login, password) => actionPromise('reg', gql(`mutation reg($login: String, $password: String) { UserUpsert(user: {login: $login, password: $password}) { _id createdAt } }`, {"login" : login, "password": password} )) const actionLogin = (login, password) => actionPromise('login', gql(`query log($login:String, $password:String) { login(login:$login, password:$password) }`, {login, password})); const OrderHistory = () => actionPromise('OrderHistory', gql(`query historyOfOrders { OrderFind(query:"[{}]") { _id total createdAt total }}`, {query: JSON.stringify([{}])} )) const orders = () => gql(`query myOrders { OrderFind(query:"[{}]"){ _id total orderGoods{ price count total good{ _id name images{ url } } } } }`, {}) const actionOrders = () => actionPromise('myOrders', orders()) const actionOrder = () => async (dispatch, getState) => { const order = Object.values(getState().cart).map(orderGoods => ({good: {_id: orderGoods.good._id}, count: orderGoods.count})); const newOrder = await dispatch(actionPromise('newOrder', gql(`mutation newOrder($order:OrderInput) { OrderUpsert(order:$order) { _id createdAt total } }`, {order: {orderGoods: order}}))); if(newOrder) { dispatch(actionCartClear()); basket() } } store.subscribe(() => { const {status, payload, error} = store.getState().promise.rootCats if (status === 'PENDING'){ main.innerHTML = `` } if (status === 'FULFILLED'){ aside.innerHTML = '' for (const {_id, name} of payload){ aside.innerHTML += `${name}` } } }) store.subscribe(() => { const token = store.getState().auth.token if (jwtDecode(token)){ reg.innerHTML='Мои Заказы' login.innerHTML=`` }else{ reg.innerHTML='Регистрация' login.innerHTML='Логин' } }) store.subscribe(() => { const {status, payload} = store.getState().promise?.myOrders || {} const [,route] = location.hash.split('/') if(route !== 'orderhistory') { return } if (status === 'PENDING'){ main.innerHTML = `` } if (status === 'FULFILLED'){ main.innerHTML = '' let i = 1 for (const goods of payload){ let divOrders = document.createElement('div') divOrders.style="border: 2px solid #ebebeb;margin: 30px;" let numberOrder = document.createElement('h1') numberOrder.innerText=`Заказ № ${i}\n` divOrders.append(numberOrder) for(const obj of goods.orderGoods){ const { price, count, total,good}=obj const {_id,name,}=good let div = document.createElement('div') let button = document.createElement('button') let a = document.createElement('a') let p = document.createElement('p') div.style="border: 2px solid #ebebeb;margin: 30px;" a.href=`#/good/${_id}` a.innerText=`${name}` p.innerText=`Стоимость товара ${price} грн\nКолличество ${count} шт\nСтоимость заказа ${total} грн\n` div.append(a,p) button.onclick= ()=>store.dispatch(actionCartAdd({_id: _id, price:price, name:name})) button.innerText='Добавить в корзину' div.append(button) divOrders.append(div) } main.prepend(divOrders) i++ } let h1 = document.createElement('h1') h1.innerText='История Заказов' main.prepend(h1) } }) store.subscribe(() => { const {status, payload, error} = store.getState().promise?.categoryGoods || {} const [,route] = location.hash.split('/') if(route !== 'category') { return } if (status === 'PENDING'){ main.innerHTML = `` } if (status === 'FULFILLED'){ main.innerHTML = '' const {name, goods} = payload main.innerHTML = `
${description}
` for (const img of images) { main.innerHTML += `` } main.innerHTML += `${price} грн