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; } 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 actionGoodById = _id => actionPromise('goodById', gql(backendURL,`query goodByID($id: String) { GoodFindOne(query: $id) { _id name description price images { url } owner { nick avatar { url text } } categories { name subCategories { name goods { name price } } } } }`, {"id": JSON.stringify([{_id}])})) function promiseReducer(state = {}, { type, name, status, payload, error }) { if (type === "PROMISE") { return { ...state, //.......скопировать старый state [name]: { status, payload, error }, //....... перекрыть в нем один 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-а}} } const store = createStore(combineReducers({auth: authReducer, promise: promiseReducer})) //не забудьте 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())) 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 url = 'http://shop-roles.node.ed.asmer.org.ua' const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/graphql' const actionRootCats = () => actionPromise('rootCats', gql(backendURL, `query { CategoryFind(query: "[{\\"parent\\":null}]"){ _id name } }`)) const actionCatById = (_id) => //добавить подкатегории actionPromise('catById', gql(backendURL, `query catById($q: String){ CategoryFindOne(query: $q){ _id name goods { _id name price images { url } } subCategories { name _id } } }`, {q: JSON.stringify([{_id}])})) const actionLogin = (login, password) => actionPromise('login', gql(backendURL,`query log($login:String, $password:String){ login(login:$login, password:$password) }`, {login, password})) store.dispatch(actionRootCats()) store.dispatch(actionLogin('test456', '123123')) //show root categories aside store.subscribe( () => { const rootCats = store.getState().promise.rootCats?.payload?.data.CategoryFind if (rootCats){ aside.innerHTML = '' for (let {_id, name} of rootCats){ const a = document.createElement('a') a.href = `#/category/${_id}` a.innerHTML = name aside.append(a) } } }) // store.subscribe(() => { // const catById = store.getState().catById?.payload?.data.CategoryFindOne // }) //cards showing store.subscribe( () => { let {catById} = store.getState().promise; const [,route, _id] = location.hash.split('/') let subCats = catById.payload?.data.CategoryFindOne.subCategories if(catById?.status === 'PENDING') { // if pending, show loading gif main.innerHTML = `` } else if(catById?.payload && route === 'category'){ // if pending stopped main.innerHTML = '' const {name, id, goods} = catById.payload?.data.CategoryFindOne main.innerHTML = `

${name}

` if(subCats.length > 0) { for(let sub of subCats) { console.log(sub) let subCat = document.createElement('a') subCat.text = sub.name subCat.href = `#/category/${sub._id}` main.append(subCat) } } // ask about showing subcats console.log(store.getState().promise) for(let good of goods) { let {name, price, images} = good let card = document.createElement('div') card.className = 'card' let cardBody = document.createElement('div') let title = document.createElement('p') title.className = 'title' let img = document.createElement('img') let desc = document.createElement('p') let cardPrice = document.createElement('p') title.innerHTML = name; // img.src = `${url}/${images[0].url}` //ACTIVATE THIS CODE AFTER FINISHING cardPrice.innerHTML = `${price} UAH` cardBody.append(title, desc, cardPrice) //ADD IMG card.append(cardBody) main.append(card); } } }); //SHOW GOOD store.subscribe(() => { // console.log(store.getState().promise) let {goodById} = store.getState().promise }) window.onhashchange = () => { const [,route, _id] = location.hash.split('/') const routes = { category(){ store.dispatch(actionCatById(_id)) }, good(){ store.dispatch(actionGoodById(_id)); } } console.log(route) if (route in routes) { routes[route]() } routes.good() console.log(route) } window.onhashchange()