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 = `

${name}

`; if (catById.payload?.subCategories) { for (const { _id, name } of catById.payload.subCategories) { const link = document.createElement("a"); link.href = `#/category/${_id}`; link.innerText = name; main.append(link); main.innerHTML += "
"; } } if (catById.payload?.goods) { for (const { _id, name, price, images } of catById.payload.goods) { const card = document.createElement("div"); const link = document.createElement("a"); card.innerHTML = `

${name}


${price}$
`; link.innerText = name; link.href = `#/good/${_id}`; card.appendChild(link); main.append(card); } } } }); store.subscribe(() => { const { goodById } = store.getState(); const [, route, _id] = location.hash.split("/"); if (goodById?.payload && route === "good") { main.innerHTML = ""; const { name, description, price, images } = goodById.payload; main.innerHTML = `

${name}

${price}$

${description}

`; } }); //store.dispatch(actionPromise('', delay(1000))) //store.dispatch(actionPromise('delay2000', delay(2000))) //store.dispatch(actionPromise('luke', fetch('https://swapi.dev/api/people/1/').then(res => res.json())))