function createStore(reducer){ let state = reducer(undefined, {}) let cbs = [] function dispatch(action){ if (typeof action === 'function'){ return action(dispatch) } const newState = reducer(state, action) if (state !== newState){ state = newState cbs.forEach(cb => cb()) } } return { dispatch, subscribe(cb){ cbs.push(cb) return () => cbs = cbs.filter(c => c !== cb) }, getState(){ return state } } } function combineReducers(reducers={cart: cartReducer, promise: promiseReducer, auth: authReducer}){ return (state={}, action) => { let newState = {} for (let key in reducers) { let newSubState = reducers[key](state[key], action) if(newSubState !== state[key]) { newState[key] = newSubState } } if (Object.keys(newState).length) { return {...state, ...newState} } else { return state } } } //promise 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 delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms)) const actionPromise = (name, promise) => async dispatch => { dispatch(actionPending(name)) try{ let payload = await promise dispatch(actionResolved(name, payload)) return payload } catch(error){ dispatch(actionRejected(name, error)) } } const getGQL = url => { return function(query, variables={}) { return fetch(url, { method: "POST", headers: {"Content-Type": "application/json", ...(localStorage.authToken ? {Authorization: 'Bearer ' + localStorage.authToken} : {}) }, body: JSON.stringify({query, variables}) }).then(resp => resp.json()) .then(data => { if ("errors" in data) { let error = new Error('ашипка, угадывай што не так') throw error } else { return data.data[Object.keys(variables)[0]] } }) } } let shopGQL = getGQL('http://shop-roles.asmer.fs.a-level.com.ua/graphql') const goodById = goodId => { let id = `[{"_id":"${goodId}"}]` return shopGQL(` query good($id:String){ GoodFindOne(query: $id) { _id name description price images { _id text url } categories { _id name } } }`, {GoodFindOne: '', id }) } const actionGoodById = id => actionPromise('goodById', goodById(id)) const actionRootCategories = () => actionPromise('rootCategories', shopGQL(` query cats($query:String){ CategoryFind(query:$query){ _id name } } `, {CategoryFind:'', query: JSON.stringify([{parent:null}])})) const actionCategoryById = (_id) => actionPromise('catById', shopGQL(` query catById($query:String){ CategoryFindOne(query:$query){ _id name goods{ _id name price description images{ url } } } }`, { CategoryFindOne: '', query: JSON.stringify([{ _id }]) })) const actionAddOrder = (cart) => { let string = '[' for (let goodId in cart) { string += `{good: {_id: "${goodId}"}, count: ${cart[goodId].count}},` } string = string.slice(0, string.length - 1) string += ']' store.dispatch(actionPromise('orderAdd', shopGQL(` mutation { OrderUpsert(order: { orderGoods: ${string} }) { _id total } }`, {OrderUpsert: ''}))) } //cart function cartReducer(state={}, {type, good, price, count=1}) { if (Object.keys(state).length === 0 && localStorage.cart?.length > 10) { let newState = JSON.parse(localStorage.cart) return newState } const types = { CART_ADD() { let newState = { ...state, [good._id]: {count: (state[good._id]?.count || 0) + count, good: {id: good._id, name: good.name}, price} } localStorage.cart = JSON.stringify(newState) return newState }, CART_REMOVE() { let {[good._id]:poh, ...newState} = state localStorage.cart = JSON.stringify(newState) return newState // let newState = {...state} // delete newState[good._id] // return newState }, CART_CLEAR() { let newState = {} localStorage.cart = JSON.stringify(newState) return newState }, CART_SET() { let newState = { ...state, [good._id]: {count, good: {id: good._id, name: good.name}, price} } localStorage.cart = JSON.stringify(newState) return newState } } if (type in types) { return types[type]() } return state } const actionCartAdd = (_id, name, price, count) => ({type: 'CART_ADD', good: {_id, name}, price, count}) const actionCartRemove = (_id, name) => ({type: 'CART_REMOVE', good: {_id, name}}) const actionCartSet = (_id, name, price, count) => ({type: 'CART_SET', good: {_id, name}, price, count}) const actionCartClear = () => ({type: 'CART_CLEAR'}) //auth const jwt_decode = (jwt) => { let payload = jwt.split('.') return JSON.parse(atob(payload[1])) } function authReducer(state, action={}){ //.... if (state === undefined){ //добавить в action token из localStorage, и проимитировать LOGIN (action.type = 'LOGIN') if (localStorage.authToken) { action.jwt = localStorage.authToken return {token: action.jwt, payload: jwt_decode(action.jwt)} } } if (action.type === 'LOGIN'){ console.log('ЛОГИН') //+localStorage //jwt_decode return {token: action.jwt, payload: jwt_decode(action.jwt)} } if (action.type === 'LOGOUT'){ console.log('ЛОГАУТ') //-localStorage //вернуть пустой объект return {} } if (action.type === 'LOGGING_IN'){ return {loginPageHello: true} } return state } const actionLogin = (jwt) => ({type: 'LOGIN', jwt}) const thunkLogin = (login, password) => { return (dispatch) => { shopGQL(`query login($login:String, $password: String) {login(login:$login, password:$password)}`, { login, password }) .then(jwt => { if (jwt) { localStorage.authToken = jwt dispatch(actionLogin(jwt)) } else { throw new Error('wrong') } }) } } const actionLogout = () => ({type: 'LOGOUT'}) const thunkLogout = () => { return (dispatch) => { localStorage.authToken = '' localStorage.cart = '' store.dispatch(actionCartClear()) dispatch(actionLogout()) } } const actionLoggingIn = () => ({type: 'LOGGING_IN'}) //store const store = createStore(combineReducers({cart: cartReducer, promise: promiseReducer, auth: authReducer})) const unsubscribe1 = store.subscribe(() => console.log(store.getState())) store.dispatch(actionRootCategories()) window.onhashchange = () => { let {1: route, 2:id} = location.hash.split('/') if (route === 'categories'){ store.dispatch(actionCategoryById(id)) } if (route === 'good'){ store.dispatch(actionGoodById(id)) } if (route === 'login'){ store.dispatch(actionLoggingIn()) } if (route === 'cart'){ drawCart() } } window.onhashchange() function drawMainMenu(){ let cats = store.getState().promise.rootCategories.payload if (cats){ //каждый раз дорисовываются в body aside.innerText = '' for (let {_id, name} of cats){ let catA = document.createElement('a') catA.href = `#/categories/${_id}` catA.innerText = name aside.append(catA) } } } function drawHeader() { login.innerHTML = (store.getState().auth?.payload ? `${store.getState().auth.payload.sub.login} | Log out` : 'Log in') if (document.querySelector('#logout')) { logout.onclick = () => { store.dispatch(thunkLogout()) } } } function drawCart() { let cart = store.getState().cart if (!localStorage.authToken) { main.innerText = 'Залогинтесь плез' } else if (!Object.keys(cart).length) { main.innerText = 'Корзина пуста' } else { main.innerText = 'Ваша корзина: ' for (let goodId in cart) { let {good: {id, name}, price, count} = cart[goodId] let goodContainer = document.createElement('div') goodContainer.classList.add('good-container') let goodName = document.createElement('div') goodName.innerText = name let goodPrice = document.createElement('div') goodPrice.innerText = 'Стоимость: ' + price let goodCount = document.createElement('input') goodCount.type = 'number' goodCount.value = count goodCount.onchange = () => { store.dispatch(actionCartSet(id, name, price, (goodCount.value > 0 ? +goodCount.value : 1))) } let removeBtn = document.createElement('button') removeBtn.innerText = 'Удалить товар' removeBtn.onclick = () => { store.dispatch(actionCartRemove (id, name)) } goodContainer.append(goodName, goodPrice, goodCount, removeBtn) main.append(goodContainer) } let price = 0 for (let goodId in cart) { price += cart[goodId].price * cart[goodId].count } let totalPriceContainer = document.createElement('div') totalPriceContainer.innerText = 'Общая стоимость: ' + price main.append(totalPriceContainer) let setOrderBtn = document.createElement('button') setOrderBtn.innerText = 'Оформить заказ' setOrderBtn.onclick = () => { actionAddOrder(store.getState().cart) } main.append(setOrderBtn) let clearBtn = document.createElement('button') clearBtn.innerText = 'Очистить корзину' clearBtn.onclick = () => { store.dispatch(actionCartClear()) } main.append(clearBtn) } } function drawOrderSuccessful() { if (store.getState().promise.orderAdd?.status === 'RESOLVED') { let order = store.getState().promise.orderAdd.payload main.innerText = 'Заказ оформился, всё круто' let orderInfo = document.createElement('div') orderInfo.innerText = `Номер заказа: ${order._id}. Стоимость: ${order.total}` main.append(orderInfo) } } store.subscribe(drawMainMenu) store.subscribe(drawHeader) store.subscribe(() => { const {1: route, 2:id} = location.hash.split('/') if (route === 'categories'){ const catById = store.getState().promise.catById?.payload if (catById){ main.innerText = '' let categoryName = document.createElement('div') categoryName.innerText = catById.name categoryName.style.fontSize = '25px' categoryName.style.fontWeight = 'bold' main.append(categoryName) for (let {_id, name, price} of catById.goods){ let good = document.createElement('a') good.href = `#/good/${_id}` good.innerText = name let btn = document.createElement('button') btn.onclick = () => { if (!localStorage.authToken) { main.innerText = 'Залогинтесь плез' } else { store.dispatch(actionCartAdd(_id, name, price)) } } btn.style.cursor = 'pointer' btn.innerText = 'купыть' main.append(good, btn) } } } if (route === 'good'){ const goodById = store.getState().promise.goodById?.payload if (goodById){ main.innerText = '' let {name, description, price, _id} = goodById let goodName = document.createElement('div') goodName.innerText = name goodName.style.fontSize = '35px' goodName.style.fontWeight = 'bold' goodName.style.marginBottom = '25px' let goodDescription = document.createElement('div') goodDescription.innerText = description goodDescription.style.marginBottom = '25px' let goodPrice = document.createElement('div') goodPrice.innerText = 'Цена: ' + price goodPrice.style.marginBottom = '5px' let btn = document.createElement('button') btn.onclick = () => { if (!localStorage.authToken) { main.innerText = 'Залогинтесь плез' } else { store.dispatch(actionCartAdd(_id, name, price)) } } btn.style.cursor = 'pointer' btn.innerText = 'купыть' main.append(goodName, goodDescription, goodPrice, btn) } } if (route === 'login') { main.innerText = '' let inputsContainer = document.createElement('div') inputsContainer.id = 'inputs' let loginInput = document.createElement('input') loginInput.type = 'text' let loginLabel = document.createElement('span') loginLabel.innerText = 'Login:' let passwordInput = document.createElement('input') passwordInput.type = 'password' let passwordLabel = document.createElement('span') passwordLabel.innerText = 'Password:' let button = document.createElement('button') button.innerText = 'log in cyka' button.onclick = () => { if (loginInput.value && passwordInput.value){ store.dispatch(thunkLogin(loginInput.value, passwordInput.value)) } } inputsContainer.append(loginLabel, loginInput, passwordLabel, passwordInput, button) main.append(inputsContainer) if (store.getState().auth?.payload) { button.disabled = true } } if (route === 'cart') { drawCart() drawOrderSuccessful() } })