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

${name}

` if(payload.subCategories !== null){ for(let {_id,name} of subCategories){ mainCard.innerHTML += ` ` } mainCard.innerHTML += `${name}
` } } } }) //Товар store.subscribe(()=>{ let {status,payload,error} = store.getState().promise?.GoodFindOne || {} const [,route, _id] = location.hash.split('/') if(route !== "good"){ return } if(status === "PENDING"){ mainCard.innerHTML = `` } if(status === "FULFILLED"){ mainCard.innerHTML = "" if(payload && route === "good"){ let {_id,name,description,images,price} = payload mainCard.innerHTML = "" let divMain = document.createElement('div') divMain.id = "divMain" divMain.innerHTML += `

${name}

` divMain.innerHTML += `

${description}

` for(let img of images){ divMain.innerHTML +=` ` } divMain.innerHTML += `

${price} $

` divMain.innerHTML +=`` mainCard.append(divMain) buy.addEventListener("click",function(){ store.dispatch(actionCartAdd({_id, name, price: price,img:images})) }) } } }) //store = createStore(cartReducer); //store.dispatch(actionCartAdd({ _id: 'пиво', price: 50 })) //store.dispatch(actionCartAdd({ _id: 'чипсы', price: 75 })); //store.dispatch(actionCartAdd({ _id: 'пиво', price: 50 }, 5)); //store.dispatch(actionCartSet({ _id: 'чипсы', price: 75 }, 2)); //store.dispatch(actionCartSub({ _id: 'пиво', price: 50 }, 4)); //Авторизація document.getElementById("Autorization").onclick = () => location.href = `#/login`; const actionFullLogin = (login, password) => async (dispatch) => { const token = await dispatch(actionLogin(login, password)) console.log(token) if(typeof(token) === "string"){ dispatch(actionAuthLogin(token)) mainCard.innerHTML = `Hello,${login}.` afterLoginization() } else { mainCard.innerHTML = `

Bad attempt

` document.getElementById("reloadAuth").onclick = function(){ location.reload() } } //проверьте что token - строка и отдайте его в actionAuthLogin } //Регістрація document.getElementById("Registration").onclick = () => location.href = `#/register` const actionFullRegistr = (login, password) => async (dispatch) => { let userRegistr = await dispatch(actionRegistr(login,password)) if(userRegistr){ dispatch(actionFullLogin(login,password)) } else { mainCard.innerHTML = `

Bad attempt

` document.getElementById("reloadReg").onclick = function(){ location.reload() } } } //При логінізації function afterLoginization (){ document.getElementById("Registration").hidden = true document.getElementById("Autorization").hidden = true document.getElementById("logOut").hidden = false document.getElementById("history").hidden = false logOut.onclick = ()=>{ store.dispatch(actionAuthLogout()) location.reload() } mainCard.innerHTML = `

I hope you are doing great.Let's buy smth!!!

` } //Історія document.getElementById("history").onclick = ()=> {location.href = `#/history`} store.subscribe(()=>{ const [,route, _id] = location.hash.split('/') if(route !== "history"){ return } let {status,payload,error} = store.getState().promise?.HistoryOrder || {} if(status === "PENDING"){ mainCard.innerHTML = `` } if(status === "FULFILLED"){ mainCard.innerHTML = "" if(payload && route === "history"){ let i=1 let j = 0 // let arr = [] // let [createdAt,total ] = payload //console.log(createdAt,total) for( let {_id,createdAt,total,orderGoods} of payload){ let data = new Date(+createdAt).toString() let time = data.split(" ").slice(1,5).join(" ") // arr.push(+createdAt) // arr.sort((a,b)=>{ // return b - a // }) mainCard.innerHTML += `

Order №${i} at ${time}

Total: ${total}$

` i++ } //console.log(arr) } } }) //Створення заказу const actionNewOrder = () => async (dispatch,getState) => { let {cart} = getState() const orderGoods = Object.entries(cart).map(([_id,{count}])=>({ good : { _id}, count, })) console.log(orderGoods) let result = await dispatch(newOrder(orderGoods)) if(result?._id){ dispatch(actionCartClear()) } } //Корзина store.subscribe(()=>{ document.getElementById("cartIcon").onclick = function inforOfCart(){ location.href = `#/cart` mainCard.innerHTML = `` let storeCartInfo = store.getState().cart let h1 = document.createElement('h1') h1.innerText = 'Cart' let sum = 0 for(let i=0;i${name}` for(const img of storeCartInfo[nameOrder].good.img){ userOrder.innerHTML +=`imageGood` } userOrder.innerHTML +=`

Cost: ${storeCartInfo[nameOrder].good.price}$

` let buttonMinus = document.createElement("button") buttonMinus.innerText = "-" userOrder.append(buttonMinus) let input = document.createElement('input') input.type = "number" input.value = store.getState().cart[nameOrder].count userOrder.append(input) let buttonPlus = document.createElement("button") buttonPlus.innerText = "+" userOrder.append(buttonPlus) let buttonDel = document.createElement('button') buttonDel.innerText = "Delete" userOrder.append(buttonDel) buttonMinus.onclick = function(){ input.value-- store.dispatch(actionCartSub({ _id:nameOrder,name:name,price:storeCartInfo[nameOrder].good.price, img:storeCartInfo[nameOrder].good.img})) inforOfCart() } buttonPlus.onclick = function(){ input.value++ store.dispatch(actionCartAdd( {_id:nameOrder,name:name,price:storeCartInfo[nameOrder].good.price, img:storeCartInfo[nameOrder].good.img}, input.value)) inforOfCart() } input.oninput = function(){ store.dispatch(actionCartSet({_id:nameOrder,name:name,price:storeCartInfo[nameOrder].good.price},+input.value)) inforOfCart() } buttonDel.onclick = function(){ store.dispatch(actionCartDel({_id:nameOrder})) inforOfCart() } sum += storeCartInfo[nameOrder].good.price * storeCartInfo[nameOrder].count } let divResult = document.createElement("div") divResult.className = "cartResult" divResult.innerHTML += `Sum: ${sum} $` mainCard.prepend(h1,divResult) let btnCreatOrd = document.createElement("button") btnCreatOrd.id = "btnCreateOrd" btnCreatOrd.innerText = "Create Order" divResult.append(btnCreatOrd) btnCreatOrd.onclick = function (){ store.dispatch(actionNewOrder()) store.dispatch(actionCartClear()) inforOfCart() } if(Object.entries(storeCartInfo).length === 0){ mainCard.innerHTML += `` mainCard.innerHTML +=`

The cart is clear

` document.getElementById("btnCreateOrd").disabled = true } } }) //LoginForm window.onhashchange = () => { const [,route, _id] = location.hash.split('/') const routes = { category() { store.dispatch(actionCatFindOne(_id)) }, good(){ // тут был store.dispatch goodById store.dispatch(actionGoodFindOne(_id)) console.log('good', _id) }, login(){ mainCard.innerHTML = `

Autorization



` // let aut = new formLogPas(mainCard,true) document.getElementById("loginButton").onclick = function() { store.dispatch(actionFullLogin(inputLogin.value, inputPassword.value)) } //нарисовать форму логина, которая по нажатию кнопки Login делает store.dispatch(actionFullLogin(login, password)) }, register(){ mainCard.innerHTML = `

Registration



` //let reg = new formLogPas(mainCard,true) document.getElementById("regButton").onclick =() => { store.dispatch(actionFullRegistr(loginReg.value, passwordReg.value)) } //нарисовать форму регистрации, которая по нажатию кнопки Login делает store.dispatch(actionFullRegister(login, password)) }, history(){ store.dispatch(HistoryOrder()) }, cart(){}, } if (route in routes){ routes[route]() } } const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms)) window.onhashchange() //const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2M2QyODUyNGRhODExZTMzNWJmY2ZlNTYiLCJsb2dpbiI6InZhczI0MyIsImFjbCI6WyI2M2QyODUyNGRhODExZTMzNWJmY2ZlNTYiLCJ1c2VyIl19LCJpYXQiOjE2NzQ3NDExNDF9.dVileTOVOJF1o6CUKgASIlTW7vdqDNLM3tycKXTIQHA"