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 в объект } } //JWT const jwt = (token) =>{ try { let payload = JSON.parse(atob(token.split(".")[1])) return payload } catch(error){ return undefined } } const reducers = { promise: promiseReducer, //допилить много имен для многих промисов auth: localStoredReducer(authReducer,"auth"), //часть предыдущего ДЗ cart: localStoredReducer(cartReducer,"cart"), //часть предыдущего ДЗ } //promiseReducer const totalReducer = combineReducers(reducers) function promiseReducer(state={}, {type,name, status, payload, error}){ if (type === 'PROMISE'){ //имена добавить return { ...state, [name] : {status, payload, error} } } return state } //CombineReducer 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 getGql (endpoint){ return async function gql(query, variables={}) { let headers = { 'Content-Type': 'application/json;charset=utf-8', 'Accept': 'application/json', } if ("authToken" in localStorage) { headers.Authorization = "Bearer " + localStorage.authToken } let result = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify({ query, variables }) }).then(res => res.json()) if (("errors" in result) && !("data" in result)) { throw new Error(JSON.stringify(result.errors)) } result = Object.values(result.data)[0] return result } } gql = getGql('http://shop-roles.node.ed.asmer.org.ua/graphql/') //authReducer function authReducer (state={},{type,token}){ if(type === "AUTH_LOGIN"){ try{ window.localStorage.setItem("authToken",token) return { token: token, payload : jwt(token) } } catch(e){ console.log(e) } } if(type === "AUTH_LOGOUT"){ window.localStorage.removeItem("authToken") return {} } return state } //cartReducer function cartReducer (state={},{type,good,count=1}){ if(type === 'CART_ADD'){ return{ ...state, [good._id]: { good, count : +count } } } if(type === "CART_SUB"){ if(state[good._id].count-count <= 0){ delete state[good._id] } else { return { ...state, [good._id] : { good, count: state[good._id].count - count, } } } } if(type === "CART_DEL"){ delete state[good._id] return { ...state } } if(type==="CART_SET"){ if(state[good._id].count <= 0){ delete state[good.id] } else { return { ...state, [good._id] : { good, count } } } } if(type === "CART_CLEAR"){ return {} } return state } //имена добавить const actionPromise = (name,promise) => async dispatch => { dispatch(actionPending(name)) //сигнализируем redux, что промис начался try{ const payload = await promise //ожидаем промиса dispatch(actionFulfilled(name,payload)) //сигнализируем redux, что промис успешно выполнен return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса } catch (error){ dispatch(actionRejected(name,error)) //в случае ошибки - сигнализируем redux, что промис несложился } } //ActionPromise 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}) //ActionLogin const actionAuthLogin = token => ({type: 'AUTH_LOGIN', token}) const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'}) //ActionCart 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'}) //LocalStoredReducer function localStoredReducer(originalReducer, localStorageKey){ function wrapper(state, action){ if(!state){ try{ return JSON.parse(localStorage[localStorageKey]) } catch {} } let resp = originalReducer(state,action) localStorage[localStorageKey] = JSON.stringify(resp) return resp } return wrapper } const store = createStore(totalReducer) store.subscribe(() => console.log(store.getState())) //const store = createStore(localStoredReducer(cartReducer, 'cart')) //store = createStore(totalReducer) //не забудьте combineReducers если он у вас уже есть //store.subscribe(() => console.log(store.getState())) let queryFindRoots = `query findRoots($q:String) { CategoryFind(query: $q){ _id name parent{ _id name } } }` let variablesFindRoots = { q:JSON.stringify([{parent:null}]) } //gql(queryFindRoots , variablesFindRoots ).then(console.log) const actionFindRoots = () => actionPromise("findRoots",gql(queryFindRoots, variablesFindRoots)) store.dispatch(actionFindRoots()) let queryCatFindOne = `query categoryFindOne ($q : String) { CategoryFindOne(query: $q){ _id name parent{ _id name } goods{ _id name description price images{ url } } subCategories{ _id name } } } ` const actionCatFindOne = (_id) => actionPromise("CatFindOne",gql(queryCatFindOne,{ q:JSON.stringify([{ _id }]) } )) let queryGoodFindOne = `query goodfindOne ($q : String) { GoodFindOne(query: $q){ _id name price description images{ url } } }` const actionGoodFindOne = (_id) => actionPromise("GoodFindOne",gql(queryGoodFindOne, { q:JSON.stringify([{ _id }]) })) //Запрос на регістрацію let mutationRegistr = `mutation registration($login:String, $password: String) { UserUpsert(user : {login:$login, password: $password}){ _id createdAt login } }` const actionRegistr = (login,password) => actionPromise("Registr",gql( mutationRegistr, {"login" : login , "password" : password})) //Зaпрос на користувача let queryLogin = `query login($login:String,$password:String){ login(login:$login,password:$password) }` const actionLogin = (login,password) => actionPromise("Login",gql(queryLogin, {login,password})) //Історія замовлень let queryHistoryOrder = `query historyOrder($order : String){ OrderFind(query: $order){ _id createdAt total owner { _id createdAt login nick } orderGoods{ _id count good { _id createdAt name description price } total } } } ` const HistoryOrder = () => actionPromise("HistoryOrder",gql(queryHistoryOrder, { order:JSON.stringify([{}]) } )) //Заказ const newOrder = (orderGoods) => actionPromise("NewOrder",gql( `mutation NewOrder($order: OrderInput) { OrderUpsert(order: $order) { _id orderGoods { _id price count total good { name _id price images { url } } } } }`, { order: { orderGoods } } ) ) // Лічильник в корзині store.subscribe (()=>{ let counter = 0 for(let {count} of Object.values(store.getState().cart)){ counter += count } cartIcon.innerHTML = counter; }) //Анон чи Логін store.subscribe(()=>{ loginName.innerHTML = store.getState().auth.payload?.sub?.login || "anon" }) //Якщо вже отримав токен в минулий раз store.subscribe(()=>{ let result = store.getState().auth?.token if(typeof(result)==="string"){ afterLoginization() } }) //URL for img const url = 'http://shop-roles.node.ed.asmer.org.ua/' store.subscribe(()=>{ let {status,payload,error} = store.getState().promise?.findRoots if(status === "PENDING"){ aside.innerHTML = "" } if(status === "FULFILLED"){ aside.innerHTML = "" for(let {_id,name} of payload){ let div = document.createElement('div') div.innerHTML = `${name}` div.classList.add('asideLink') document.getElementById('aside').append(div) } } }) //Категорії store.subscribe(()=>{ let {status,payload,error} = store.getState().promise?.CatFindOne || {} const [,route, _id] = location.hash.split('/') if(route !== "category"){ return } if(status === "PENDING"){ mainCard.innerHTML = `` } if(status === "FULFILLED"){ const {name,subCategories,goods} = payload mainCard.innerHTML = "" if(payload && route === "category"){ mainCard.innerHTML += `