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 promiseReducer(state = {}, { type, status, payload, error, name }) { if (type === "PROMISE") { return { ...state, [name]: { status, payload, error }, }; } return state; } const actionPending = (name) => ({ type: "PROMISE", status: "PENDING", name }); const actionResolved = (name, payload) => ({ type: "PROMISE", status: "RESOLVED", name, payload, }); const actionRejected = (name, error) => ({ type: "PROMISE", status: "REJECTED", name, error, }); 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)); } }; const store = createStore(promiseReducer); store.subscribe(() => console.log(store.getState())); const delay = (ms) => new Promise((ok) => setTimeout(() => ok(ms), ms)); const getGQL = (url) => async (query, variables = {}) => { let obj = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...(localStorage.authToken ? { Authorization: "Bearer " + localStorage.authToken } : {}), }, 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 }]) } ) ); store.dispatch(actionRootCats()); store.dispatch(actionGoodById()); store.subscribe(() => { const { rootCats } = store.getState(); 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); } } }); window.onhashchange = () => { const [, route, _id] = location.hash.split("/"); const routes = { category() { store.dispatch(actionCatById(_id)); }, good() { store.dispatch(actionGoodById(_id)); }, }; if (route in routes) routes[route](); }; window.onhashchange(); store.subscribe(() => { const { catById } = store.getState(); const [, route, _id] = location.hash.split("/"); if (catById?.payload && route === "category") { const { name } = catById.payload; main.innerHTML = `
${price}$
${description}