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, name, status, payload, error}){ ////????? //ОДИН ПРОМИС: //состояние: PENDING/FULFILLED/REJECTED //результат //ошибка: //{status, payload, error} //{ // name1:{status, payload, error} // name2:{status, payload, error} // name3:{status, payload, error} //} if (type === 'PROMISE'){ return { ...state, [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 goToMainPage = () => location.href = location.href.split("#")[0]; const checkAuthToken = () => { const headers = { "Content-Type": "application/json", "Accept": "application/json", } if(localStorage.getItem('authToken')) { return { ...headers, "Authorization": `Bearer ${localStorage.getItem('authToken')}` } } else { return headers; } } const getGQL = url => (query, variables= {}) => fetch(url, { method: 'POST', headers: checkAuthToken(), body:JSON.stringify({query, variables}) }).then(res => res.json()) .then(data => { try { if(!data.data && data.errors) { throw new SyntaxError(`SyntaxError - ${JSON.stringify(Object.values(data.errors)[0])}`); } return Object.values(data.data)[0]; } catch (e) { console.error(e); } }); 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 } } } if (type === 'AUTH_LOGOUT'){ //мы разлогиниваемся return {} } return state } const actionAuthLogout = () => dispatch => { dispatch({type: 'AUTH_LOGOUT'}); localStorage.removeItem('authToken'); goToMainPage() } const actionAuthLogin = (token) => (dispatch, getState) => { const oldState = getState() dispatch({type: 'AUTH_LOGIN', token}) const newState = getState() if (oldState !== newState) localStorage.setItem('authToken', token) } function cartReducer(state={}, {type, amount=1, good}){ /* { id1: {amount, good: {объект с бэка с _id, description, name, price}}, id2: {amount, good: {объект с бэка с _id, description, name, price}}, id3: {amount, good: {объект с бэка с _id, description, name, price}} } */ if (type === 'CART_ADD'){ return { ...state, [good._id]: {good, amount: (state[good._id]?.amount || 0) + amount } } } if (type === 'CART_SET'){ return { ...state, [good._id]: {good, amount} } } if (type === 'CART_DELETE'){ const {[good._id]: skip,...newState} = state //const newState = { ...state } //delete newState[good._id] return newState } if (type === 'CART_CLEAR'){ return {} } //if (type === ''){ //} return state } const actionCartAdd = (good, amount=1) => ({type: 'CART_ADD', good, amount}) const actionCartSet = (good, amount=1) => ({type: 'CART_SET', good, amount}) const actionCartClear = () => ({type: 'CART_CLEAR'}) const actionCartDelete = (good) => ({type: 'CART_DELETE', good}) function combineReducers(reducers){ function totalReducer(state={}, action){ //{ //promise:{ //name1:{status, payload, error}, //name2:{status, payload, error} //}, //auth: { //token, payload //} //} 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 localStoredReducer(reducer, localStorageKey){ function wrapperReducer(state, action){ if (state === undefined){ //если загрузка сайта try { return JSON.parse(localStorage[localStorageKey]) //пытаемся распарсить сохраненный //в localStorage state и подсунуть его вместо результата редьюсера } catch(e){ } //если распарсить не выйдет, то код пойдет как обычно: } const newState = reducer(state, action) localStorage.setItem(localStorageKey, JSON.stringify(newState)) //сохраняем состояние в localStorage return newState } return wrapperReducer } const reducers = { auth: authReducer, cart: localStoredReducer(cartReducer, 'cart'), //в localStorage должен появится ключ cart с JSON стейта корзины promise: localStoredReducer(promiseReducer, 'promise'), } const totalReducer = combineReducers(reducers) const store = createStore(totalReducer) //не забудьте combineReducers если он у вас уже есть store.subscribe(() => console.log(store.getState())) const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/' const gql = getGQL(backendURL + 'graphql') const rootCategories = () => gql(`query cadz($q:String) { CategoryFind(query:$q){ _id name } }`, {q: JSON.stringify([{parent: null}])}) const actionRootCategories = () => actionPromise('rootCategories', rootCategories()) const categoryById = _id => //добавьте сюда подкатегории и родителя - пригодятся gql(`query catById($qCat:String){ CategoryFindOne(query:$qCat){ _id name parent { _id name } subCategories { name _id parent { _id name } } goods{ _id name price images{ url } } } }`, {qCat: JSON.stringify([{_id}])}) const actionCategoryById = _id => actionPromise('catById', categoryById(_id)) const goodById = _id => gql(`query goodById($goodId:String) { GoodFindOne(query:$goodId) { _id name price description images { url } } }`, {goodId: JSON.stringify([{_id}])}) const actionGoodById = _id => actionPromise('goodById', goodById(_id)); const actionOrder = () => async (dispatch, getState) => { const order = Object.values(getState().cart).map(orderGoods => ({good: {_id: orderGoods.good._id}, count: orderGoods.amount})); const myOrder = await dispatch(actionPromise('myOrder', gql(`mutation myOrder($order:OrderInput) { OrderUpsert(order:$order) { _id createdAt total } }`, {order: {orderGoods: order}}))); if(myOrder) { dispatch(actionCartClear()); } } const orders = () => gql(`query myOrders { OrderFind(query:"[{}]"){ _id total orderGoods{ price count total good{ _id name images{ url } } } } }`, {}) const actionOrders = () => actionPromise('myOrders', orders()) const actionLogin = (login, password) => actionPromise('login', gql(`query log($login:String, $password:String) { login(login:$login, password:$password) }`, {login, password})); const actionRegister = (login, password) => actionPromise('register', gql(`mutation register($login:String, $password:String) { UserUpsert(user:{login:$login, password:$password}) { _id login createdAt } }`, {login, password})); const actionFullLogin = (login, password) => async dispatch => { const token = await dispatch(actionLogin(login, password)) if (token){ dispatch(actionAuthLogin(token)); goToMainPage() } } const actionFullRegister = (login, password) => async dispatch => { const user = await dispatch(actionRegister(login, password)) if(user) { dispatch(actionFullLogin(login, password)) } } const actionFullOrders = () => async dispatch => { await dispatch(actionOrders()); if(Object.keys(store.getState().auth).length === 0) { goToMainPage() } } function Password(parent, open) { const input = document.createElement('input'); input.id = 'password' input.type = 'password'; parent.appendChild(input); const button = document.createElement('button'); button.type = 'button'; button.textContent = 'показать'; parent.appendChild(button); button.addEventListener('click', () => { this.setOpen(open !== true); }); this.setValue = newValue => input.value = newValue; this.getValue = () => input.value; this.setOpen = openUpdate => { open = openUpdate; if(typeof this.onOpenChange === 'function') { this.onOpenChange(openUpdate); } button.textContent = (openUpdate) ? 'показать' : 'скрыть'; input.type = (openUpdate) ? 'password' : 'text'; } this.getOpen = () => open; input.addEventListener('input', event => { if (typeof this.onChange === 'function'){ this.onChange(event.target.value); } }); } function LoginForm(parent) { const createDivider = () => parent.appendChild(document.createElement('br')); const input = document.createElement('input'); input.id = 'login'; input.type = 'text'; parent.appendChild(input); const button = document.createElement('button'); button.type = 'button'; button.textContent = 'Логин'; button.disabled = true; input.addEventListener('input', event => { if (typeof this.onChange === 'function'){ this.onChange(event.target.value); } }); button.addEventListener('click', event => { if (typeof this.onLogin === 'function') { this.onLogin(event.target); } }); this.getLogin = () => input.value; createDivider(); const password = new Password(parent, true); const getPassword = () => password.getValue(); createDivider(); parent.appendChild(button); const isDisabled = () => button.disabled = (!(getPassword() !== '' && this.getLogin() !== '')); password.onChange = () => isDisabled(); this.onChange = () => isDisabled(); this.setButtonText = newText => button.textContent = newText; } store.subscribe(() => { const rootCats = store.getState().promise.rootCategories?.payload if (rootCats){ aside.innerHTML = '' for (let {_id, name} of rootCats){ const a = document.createElement('a') a.innerText = name a.href = `#/category/${_id}` aside.append(a) } } }) store.subscribe(() => { const catById = store.getState().promise.catById?.payload const [,route] = location.hash.split('/') if (catById && route === 'category'){ const {name, goods, parent, subCategories} = catById; main.innerHTML = `
${description}
${price}Количество - ${good.amount} шт.
Итого по позиции - ${price * good.amount}
Итого - ${value.total}
`; for (const goodElement of Object.values(value.orderGoods)) { const {price, count, total, good} = goodElement; const orderCartElement = document.createElement('div'); orderCartElement.classList.add('order-cart__element'); orderCartElement.innerHTML = `Цена - ${price}
Количество - ${count}
Итого по позиции - ${total}