{//все готово до выполнения модуля // // delay // const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms)) // // createStore // 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 в объект // } // } // // ======================================================================================================== // // promiseReducer // function promiseReducer(state = {}, { type, status, payload, error, nameOfPromise }) { // if (type === 'PROMISE') { // return { // ...state, // [nameOfPromise]: { status, payload, error } // } // } // return state // } // // Акшоны: // // Соответственно, имя промиса будет передаваться в редьюсер как ключ в объекте action, а так же: // const actionPending = nameOfPromise => ({ nameOfPromise, type: 'PROMISE', status: 'PENDING' }) // в actionPending - единственный параметр // const actionFulfilled = (nameOfPromise, payload) => ({ nameOfPromise, type: 'PROMISE', status: 'FULFILLED', payload }) // как первый параметр, payload станет вторым параметром // const actionRejected = (nameOfPromise, error) => ({ nameOfPromise, type: 'PROMISE', status: 'REJECTED', error }) // по аналогии с actionFulfilled // const actionPromise = (nameOfPromise, promise) => // первый параметр, второй параметр - сам промис, который мы будем обрабатывать; // async dispatch => { // dispatch(actionPending(nameOfPromise)) //сигнализируем redux, что промис начался // try { // const payload = await promise //ожидаем промиса // dispatch(actionFulfilled(nameOfPromise, payload)) //сигнализируем redux, что промис успешно выполнен // return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса // } // catch (error) { // dispatch(actionRejected(nameOfPromise, error)) //в случае ошибки - сигнализируем redux, что промис несложился // } // } // //========================================================================================================= // // authReducer // const jwtDecode = function (token) { // try { // let parseData = token.split('.')[1] // return JSON.parse(atob(parseData)) // } // catch (e) { // return undefined // } // } // function authReducer(state = {}, { type, token }) { // if (type === 'AUTH_LOGIN') { // let payload = jwtDecode(token) // return state = { // token, // payload // } // } // if (type === 'AUTH_LOGOUT') { // return {} // } // return state // } // // проверка (и экшенкриейторы бонусом): // const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token }) // const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' }) // // const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2Mzc3ZTEzM2I3NGUxZjVmMmVjMWMxMjUiLCJsb2dpbiI6InRlc3Q1IiwiYWNsIjpbIjYzNzdlMTMzYjc0ZTFmNWYyZWMxYzEyNSIsInVzZXIiXX0sImlhdCI6MTY2ODgxMjQ1OH0.t1eQlRwkcP7v9JxUPMo3dcGKprH-uy8ujukNI7xE3A0" // //========================================================================================================= // // cartReducer // function cartReducer(state = {}, { type, count, good }) { // if (type === 'CART_ADD') { // return { // ...state, // [good._id]: { // good, // count: (state[good._id] ? state[good._id].count + count : count) // } // } // } // if (type === 'CART_SUB') { // if (state[good._id]) { // let newCount = state[good._id].count - count // if (newCount > 0) { // return { // ...state, // [good._id]: { // good, // count: newCount // } // } // } else { // delete state[good._id] // return { ...state } // } // } else { // return undefined // } // } // if (type === 'CART_DEL') { // delete state[good._id] // return { ...state } // } // if (type === 'CART_SET') { // if (count > 0) { // return { // ...state, // [good._id]: { // good, // count // } // } // } else { // delete state[good._id] // return { ...state } // } // } // if (type === 'CART_CLEAR') { // return state = {} // } // return state // } // // экшоны: // // Добавление товара.Должен добавлять новый ключ в state, или обновлять, если ключа в state ранее не было, увеличивая количество // const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', count, good }) // // Уменьшение количества товара.Должен уменьшать количество товара в state, или удалять его если количество будет 0 или отрицательным // const actionCartSub = (good, count = 1) => ({ type: 'CART_SUB', count, good }) // // Удаление товара.Должен удалять ключ из state // const actionCartDel = (good) => ({ type: 'CART_DEL', good }) // // Задание количества товара.В отличие от добавления и уменьшения, не учитывает того количества, которое уже было в корзине, а тупо назначает количество поверху(или создает новый ключ, если в корзине товара не было).Если count 0 или отрицательное число - удаляем ключ из корзины; // const actionCartSet = (good, count = 1) => ({ type: 'CART_SET', count, good }) // // Очистка корзины.state должен стать пустым объектом { } // const actionCartClear = () => ({ type: 'CART_CLEAR' }) // //========================================================================================================= // // ======================================================================================================== // // GraphQL запросы // // Запрос на список корневых категорий // const findCategory = `query baseCategory($searchVariablesCategory: String){ // CategoryFind(query: $searchVariablesCategory){ // _id name parent { // _id // name // } // } // }` // const findCategoryVar = { // searchVariablesCategory: JSON.stringify([{ parent: null }]) // } // const actionCategoryFind = () => actionPromise('CategoryFind', gql(findCategory, findCategoryVar)) // //========================================================================================================= // // Запрос для получения одной категории с товарами и картинками // const findOneCategory = `query categoryFindOne($searchVariablesCategoryOne: String,) { // CategoryFindOne(query: $searchVariablesCategoryOne){ // _id name parent{ // _id name // } // goods{ // _id name description price // images{ // url // } // } // subCategories{ // _id name // } // } // }` // const findOneCategoryVar = { // searchVariablesCategoryOne: JSON.stringify([{ _id: "6262ca7dbf8b206433f5b3d1" }]) // } // const actionCategoryFindOne = () => actionPromise('CategoryFindOne', gql(findOneCategory, findOneCategoryVar)) // //========================================================================================================= // // Запрос на получение товара с описанием и картинками // const findGoodWithImage = `query oneGoodWithImages($searchVariablesGoodOne: String) { // GoodFindOne(query: $searchVariablesGoodOne){ // _id name price description images { // url // } // } // } // ` // const findGoodWithImageVar = { // searchVariablesGoodOne: JSON.stringify([{ _id: "62c9472cb74e1f5f2ec1a0d3" }]) // } // const actionGoodFindOne = () => actionPromise('GoodFindOne', gql(findGoodWithImage, findGoodWithImageVar)) // //========================================================================================================= // // Запрос на регистрацию - работает, если не залогинен пользователь // const registration = `mutation registration($loginReg:String,$passwordReg:String ){ // UserUpsert(user:{ // login:$loginReg, password:$passwordReg // }){ // _id createdAt // } // }` // const registrationVar = { // loginReg: "abababa", // вот тут нужно в параметры запихнуть переменную, которую будем получать // passwordReg: "123123" // вот тут нужно в параметры запихнуть переменную, которую будем получать // } // const actionUserUpsert = () => actionPromise('UserUpsert', gql(registration, registrationVar)) // //========================================================================================================= // // Запрос на логин // const checkLogin = `query login($login: String, $password: String){ // login(login: $login, password: $password) // } // ` // const checkLoginVar = { // login: "abababa", // вот тут нужно в параметры запихнуть переменную, которую будем получать // password: "123123" // вот тут нужно в параметры запихнуть переменную, которую будем получать // } // const actionLogin = () => actionPromise('login', gql(checkLogin, checkLoginVar)) // //========================================================================================================= // // Запрос истории заказов - нужно учитывать, что работает только, если вместе с заголовком отправить JWT-token от пользователя // const orderFind = `query order ($order: String){ // OrderFind(query: $order){ // _id total orderGoods{ // good { // _id // name // price // } // } // } // } // ` // const orderFindVar = { // order: JSON.stringify([{}]) // } // const actionOrderFind = () => actionPromise('login', gql(orderFind, orderFindVar)) // //========================================================================================================= // // Запрос оформления заказа - нужно добить: После успешного оформления заказа на бэке задиспатчить экшон очистки корзины // const orderCreate = `mutation myOrder($createOrder: OrderInput){ // OrderUpsert(order: $createOrder) { // orderGoods{ // count good{ // _id // } // } // } // }` // const orderCreateVar = { // createOrder: JSON.stringify({ orderGoods: { count: 2, good: { _id: "62c9472cb74e1f5f2ec1a0d2" } } }) // вот тут нужно в параметры запихнуть переменную, которую будем получать // } // const actionOrderUpsert = () => actionPromise('login', gql(orderCreate, orderCreateVar)) // // ======================================================================================================== // // ======================================================================================================== // // Вспомогательные функции // // getGql - переделка из HW18 функции gql (делаем запрос на бэк) // function getGql(endpoint) { // let headers = { // 'Content-Type': 'application/json;charset=utf-8', // 'Accept': 'application/json', // } // if ('authToken' in localStorage) { // headers.Authorization = 'Bearer ' + localStorage.authToken // } // return async function gql(query, variables = {}) { // 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 // } // } // //========================================================================================================= // // localStoredReducer // function localStoredReducer(originalReducer, localStorageKey) { // function wrapper(state, action) { // if (!state) { // try { // return JSON.parse(localStorage[localStorageKey]) // } // catch (error) { // } // } // const newState = originalReducer(state, action) // localStorage[localStoredReducer] = JSON.stringify(newState) // return newState // } // return wrapper // } // //========================================================================================================= // // Модуль } // createStore 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 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 } // создаем объект с редьюсерами const reducers = { promise: promiseReducer, //допилить много имен для многих промисо auth: authReducer, //часть предыдущего ДЗ cart: cartReducer, //часть предыдущего ДЗ } // скармливаем объект с редьюсерами в компайн-редьюсер const totalReducer = combineReducers(reducers) // ======================================================================================================== // promiseReducer function promiseReducer(state = {}, { type, status, payload, error, nameOfPromise }) { if (type === 'PROMISE') { return { ...state, [nameOfPromise]: { status, payload, error } } } return state } // Акшоны: // Соответственно, имя промиса будет передаваться в редьюсер как ключ в объекте action, а так же: const actionPending = nameOfPromise => ({ nameOfPromise, type: 'PROMISE', status: 'PENDING' }) // в actionPending - единственный параметр const actionFulfilled = (nameOfPromise, payload) => ({ nameOfPromise, type: 'PROMISE', status: 'FULFILLED', payload }) // как первый параметр, payload станет вторым параметром const actionRejected = (nameOfPromise, error) => ({ nameOfPromise, type: 'PROMISE', status: 'REJECTED', error }) // по аналогии с actionFulfilled const actionPromise = (nameOfPromise, promise) => // первый параметр, второй параметр - сам промис, который мы будем обрабатывать; async dispatch => { dispatch(actionPending(nameOfPromise)) //сигнализируем redux, что промис начался try { const payload = await promise //ожидаем промиса dispatch(actionFulfilled(nameOfPromise, payload)) //сигнализируем redux, что промис успешно выполнен return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса } catch (error) { dispatch(actionRejected(nameOfPromise, error)) //в случае ошибки - сигнализируем redux, что промис несложился } } const store = createStore(totalReducer) // создаем магаз с редюсером store.subscribe(() => console.log(store.getState())) // для контроля выводим все изменения в магазине в консоль //========================================================================================================= // authReducer const jwtDecode = function (token) { try { let parseData = token.split('.')[1] return JSON.parse(atob(parseData)) } catch (e) { return undefined } } function authReducer(state = {}, { type, token }) { if (type === 'AUTH_LOGIN') { let payload = jwtDecode(token) return state = { token, payload } } if (type === 'AUTH_LOGOUT') { return {} } return state } // проверка (и экшенкриейторы бонусом): const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token }) const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' }) // const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2Mzc3ZTEzM2I3NGUxZjVmMmVjMWMxMjUiLCJsb2dpbiI6InRlc3Q1IiwiYWNsIjpbIjYzNzdlMTMzYjc0ZTFmNWYyZWMxYzEyNSIsInVzZXIiXX0sImlhdCI6MTY2ODgxMjQ1OH0.t1eQlRwkcP7v9JxUPMo3dcGKprH-uy8ujukNI7xE3A0" //========================================================================================================= // cartReducer function cartReducer(state = {}, { type, count, good }) { if (type === 'CART_ADD') { return { ...state, [good._id]: { good, count: (state[good._id] ? state[good._id].count + count : count) } } } if (type === 'CART_SUB') { if (state[good._id]) { let newCount = state[good._id].count - count if (newCount > 0) { return { ...state, [good._id]: { good, count: newCount } } } else { delete state[good._id] return { ...state } } } else { return undefined } } if (type === 'CART_DEL') { delete state[good._id] return { ...state } } if (type === 'CART_SET') { if (count > 0) { return { ...state, [good._id]: { good, count } } } else { delete state[good._id] return { ...state } } } if (type === 'CART_CLEAR') { return state = {} } return state } // экшоны: // Добавление товара.Должен добавлять новый ключ в state, или обновлять, если ключа в state ранее не было, увеличивая количество const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', count, good }) // Уменьшение количества товара.Должен уменьшать количество товара в state, или удалять его если количество будет 0 или отрицательным const actionCartSub = (good, count = 1) => ({ type: 'CART_SUB', count, good }) // Удаление товара.Должен удалять ключ из state const actionCartDel = (good) => ({ type: 'CART_DEL', good }) // Задание количества товара.В отличие от добавления и уменьшения, не учитывает того количества, которое уже было в корзине, а тупо назначает количество поверху(или создает новый ключ, если в корзине товара не было).Если count 0 или отрицательное число - удаляем ключ из корзины; const actionCartSet = (good, count = 1) => ({ type: 'CART_SET', count, good }) // Очистка корзины.state должен стать пустым объектом { } const actionCartClear = () => ({ type: 'CART_CLEAR' }) //========================================================================================================= // ======================================================================================================== // GraphQL запросы // Запрос на список корневых категорий const findCategory = `query baseCategory($searchVariablesCategory: String){ CategoryFind(query: $searchVariablesCategory){ _id name parent { _id name } } }` const findCategoryVar = { searchVariablesCategory: JSON.stringify([{ parent: null }]) } const actionCategoryFind = () => actionPromise('CategoryFind', gql(findCategory, findCategoryVar)) //========================================================================================================= // Запрос для получения одной категории с товарами и картинками const findOneCategory = `query categoryFindOne($searchVariablesCategoryOne: String,) { CategoryFindOne(query: $searchVariablesCategoryOne){ _id name parent{ _id name } goods{ _id name description price images{ url } } subCategories{ _id name } } }` const findOneCategoryVar = { searchVariablesCategoryOne: JSON.stringify([{ _id: "6262ca7dbf8b206433f5b3d1" }]) } const actionCategoryFindOne = () => actionPromise('CategoryFindOne', gql(findOneCategory, findOneCategoryVar)) //========================================================================================================= // Запрос на получение товара с описанием и картинками const findGoodWithImage = `query oneGoodWithImages($searchVariablesGoodOne: String) { GoodFindOne(query: $searchVariablesGoodOne){ _id name price description images { url } } } ` const findGoodWithImageVar = { searchVariablesGoodOne: JSON.stringify([{ _id: "62c9472cb74e1f5f2ec1a0d3" }]) } const actionGoodFindOne = () => actionPromise('GoodFindOne', gql(findGoodWithImage, findGoodWithImageVar)) //========================================================================================================= // Запрос на регистрацию - работает, если не залогинен пользователь const registration = `mutation registration($loginReg:String,$passwordReg:String ){ UserUpsert(user:{ login:$loginReg, password:$passwordReg }){ _id createdAt } }` const registrationVar = { loginReg: "abababa", // вот тут нужно в параметры запихнуть переменную, которую будем получать passwordReg: "123123" // вот тут нужно в параметры запихнуть переменную, которую будем получать } const actionUserUpsert = () => actionPromise('UserUpsert', gql(registration, registrationVar)) //========================================================================================================= // Запрос на логин const checkLogin = `query login($login: String, $password: String){ login(login: $login, password: $password) } ` const checkLoginVar = { login: "abababa", // вот тут нужно в параметры запихнуть переменную, которую будем получать password: "123123" // вот тут нужно в параметры запихнуть переменную, которую будем получать } const actionLogin = () => actionPromise('login', gql(checkLogin, checkLoginVar)) //========================================================================================================= // Запрос истории заказов - нужно учитывать, что работает только, если вместе с заголовком отправить JWT-token от пользователя const orderFind = `query order ($order: String){ OrderFind(query: $order){ _id total orderGoods{ good { _id name price } } } } ` const orderFindVar = { order: JSON.stringify([{}]) } const actionOrderFind = () => actionPromise('login', gql(orderFind, orderFindVar)) //========================================================================================================= // Запрос оформления заказа - нужно добить: После успешного оформления заказа на бэке задиспатчить экшон очистки корзины const orderCreate = `mutation myOrder($createOrder: OrderInput){ OrderUpsert(order: $createOrder) { orderGoods{ count good{ _id } } } }` const orderCreateVar = { createOrder: JSON.stringify({ orderGoods: { count: 2, good: { _id: "62c9472cb74e1f5f2ec1a0d2" } } }) // вот тут нужно в параметры запихнуть переменную, которую будем получать } const actionOrderUpsert = () => actionPromise('login', gql(orderCreate, orderCreateVar)) // ======================================================================================================== // ======================================================================================================== // Вспомогательные функции // getGql - переделка из HW18 функции gql (делаем запрос на бэк) function getGql(endpoint) { let headers = { 'Content-Type': 'application/json;charset=utf-8', 'Accept': 'application/json', } if ('authToken' in localStorage) { headers.Authorization = 'Bearer ' + localStorage.authToken } return async function gql(query, variables = {}) { 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 } } //========================================================================================================= // localStoredReducer function localStoredReducer(originalReducer, localStorageKey) { function wrapper(state, action) { if (!state) { try { return JSON.parse(localStorage[localStorageKey]) } catch (error) { } } const newState = originalReducer(state, action) localStorage[localStoredReducer] = JSON.stringify(newState) return newState } return wrapper } //========================================================================================================= // Модуль