// url of backend const url = 'http://shop-roles.node.ed.asmer.org.ua/graphql' // Вспомогательные функции ======================================================================================================== // getGql - переделка из HW18 функции gql (делаем запрос на бэк) 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 } } const gql = getGql(url) //========================================================================================================= // 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[localStorageKey] = JSON.stringify(newState) return newState } return wrapper } // Redux ======================================================================================================== // 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, dispatch, subscribe } } // создание combineReducers 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 } // создание promiseReducer function promiseReducer(state = {}, { type, status, payload, error, nameOfPromise }) { if (type === 'PROMISE') { return { ...state, [nameOfPromise]: { status, payload, error } } } return state } // акшоны для promiseReducer const actionPending = nameOfPromise => ({ nameOfPromise, type: 'PROMISE', status: 'PENDING' }) const actionFulfilled = (nameOfPromise, payload) => ({ nameOfPromise, type: 'PROMISE', status: 'FULFILLED', payload }) const actionRejected = (nameOfPromise, error) => ({ nameOfPromise, type: 'PROMISE', status: 'REJECTED', error }) 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 // раскодируем JWT-токен 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') { localStorage.removeItem('authToken') return {} } return state } // акшон для логинизации const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token }) // акшон для раззлогинивания const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' }) // 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 {} } return state } // акшоны для cartReducer // Добавление товара.Должен добавлять новый ключ в state, или обновлять, если ключа в state ранее не было, увеличивая количество const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count }) // Уменьшение количества товара.Должен уменьшать количество товара в 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' }) // объект со всеми редьюсерами const reducers = { promise: localStoredReducer(promiseReducer, 'promise'), auth: localStoredReducer(authReducer, 'auth'), cart: localStoredReducer(cartReducer, 'cart'), } const totalReducer = combineReducers(reducers) const store = createStore(totalReducer) store.subscribe(() => console.log(store.getState())) // для контроля выводим все изменения в магазине в консоль // GraphQL запросы переписанные сразу в акшоны ======================================================================================================== // Запрос на список корневых категорий (без родителей) const actionCategoryFind = () => actionPromise('CategoryFind', gql(`query baseCategory($searchVariablesCategory: String){ CategoryFind(query: $searchVariablesCategory){ _id name parent { _id name } } }`, { searchVariablesCategory: JSON.stringify([{ parent: null }]) })) store.dispatch(actionCategoryFind()) // и сразу же все запустили, чтоб постоянно отрисовывалась // Запрос для получения одной категории с товарами и картинками const actionCategoryFindOne = _id => actionPromise('CategoryFindOne', gql(`query categoryFindOne($searchVariablesCategoryOne: String,) { CategoryFindOne(query: $searchVariablesCategoryOne){ _id name goods{ _id name description price images{ url } } subCategories{ _id name } } }`, { searchVariablesCategoryOne: JSON.stringify([{ _id }]) })) // Запрос на получение товара с описанием и картинками const actionGoodFindOne = _id => actionPromise('GoodFindOne', gql(`query oneGoodWithImages($searchVariablesGoodOne: String) { GoodFindOne(query: $searchVariablesGoodOne){ _id name price description images { url } } }`, { searchVariablesGoodOne: JSON.stringify([{ _id }]) })) // Запрос на логин const actionLogin = (login, password) => actionPromise('login', gql(`query login($login: String, $password: String) { login(login: $login, password: $password) }`, { login, password })) // показываем ошибку при авторизации, если неправильный логин или пароль function mistakeLogin() { main.innerHTML = `
Вы ввели неправильный логин или пароль
` const refresh = document.getElementById('refreshBtn') refresh.addEventListener('click', () => { location.reload() }) } // Запрос на логин и последующую логинизацию в authReduser (thunk) const actionFullLogin = (login, password) => async dispatch => { const token = await dispatch(actionLogin(login, password)) if (token != null) { if (typeof (token) === 'string') { dispatch(actionAuthLogin(token)) localStorage.authToken = token } } else { mistakeLogin() } } // Запрос на регистрацию const actionUserCreate = (login, password) => actionPromise('UserRegistrate', gql(`mutation registration($login:String,$password:String ){ UserUpsert(user:{ login:$login, password:$password }){ _id createdAt } }`, { login, password })) // показываем ошибку при регистрации, если логин занят function mistakeRegistration() { main.innerHTML = `Этот логин занят. Повторите регистрацию с уникальным логином или авторизуйтесь
` const refresh = document.getElementById('refreshBtn') refresh.addEventListener('click', () => { location.reload() }) } // Запрос на регистрацию и сразу на авторизацию пользователя на странице (thunk) const actionFullUserCreate = (login, password) => async dispatch => { try { const registration = await dispatch(actionUserCreate(login, password)) if (registration._id !== 'null') { dispatch(actionFullLogin(login, password)) } } catch (e) { mistakeRegistration() } } // Запрос истории заказов const actionOrderFind = () => actionPromise('OrderFind', gql(`query order($order: String){ OrderFind(query: $order){ createdAt total orderGoods{ good { name price images { url } } total count } } }`, { order: JSON.stringify([{}]) })) // запрос отправку заказа на сервер const actionOrder = (goods) => actionPromise('orderCreate', gql(`mutation myOrder($createOrder: OrderInput){ OrderUpsert(order: $createOrder) { orderGoods{ count good{ _id } } } }`, { createOrder: { orderGoods: goods } })) // запрос отправку заказа на сервер с последующей очисткой корзины (thunk) const actionFillOrder = () => async dispatch => { // создаем массив с параметрами для диспатча let arrWithGoods = [] for (const [id, { count }] of Object.entries(store.getState().cart)) { arrWithGoods.push({ count: count, good: { _id: id } }) } // оформляем заказ await dispatch(actionOrder(arrWithGoods)) // и как задиспатчится заказ чистим корзину store.dispatch(actionCartClear()) } // Модуль ========================================================================================================= // отрисовка в main списка товаров из категории const drawGoods = () => { const [, route] = location.hash.split('/') if (route !== 'category') return const { status, payload, error } = store.getState().promise.CategoryFindOne if (status === 'PENDING') { main.innerHTML = `` } if (status === 'FULFILLED') { const { _id, name, goods, subCategories } = payload // подумать, нужны ли ид и субкатегория (субкатегория может ыть нужна, чтобы отрисовать ее в ) main.innerHTML = `${description}
Создано: ${dateOfOrderParse}
Товар: ${name}
Цена товара: ${price}
Количество товара в заказе: ${count}
Итоговая сумма: ${total}
Ваша корзина пуста.Чтобы сделать заказ, сначала добавьте товары.
` } } // функция - конструктор формы логин/пароль ========================================================================================================= function LoginForm(parent) { function Password(parent, open) { // отображение формы для пароля const inputPassword = document.createElement('input') inputPassword.type = 'password' inputPassword.placeholder = 'Insert password' parent.append(inputPassword) // создание и отображение чекбокса (открыть/скрыть пароль) const inputCheckbox = document.createElement('input') inputCheckbox.type = 'checkbox' inputCheckbox.checked = false parent.append(inputCheckbox) // создаем геттеры this.getValue = () => inputPassword.value this.getOpen = () => inputCheckbox.checked // создаем сеттеры this.setValue = (value) => inputPassword.value = value this.setOpen = (open) => { if (open === true) { inputPassword.type = 'text' inputCheckbox.checked = true } if (open === false) { inputPassword.type = 'password' inputCheckbox.checked = false } return inputPassword.type, inputCheckbox.checked } // starting onChange inputPassword.addEventListener('input', () => { this.onChange(inputPassword.value) }) // starting onOpenChange + change inputPasswor hide/see inputCheckbox.addEventListener('change', () => { this.setOpen(inputCheckbox.checked) this.onOpenChange(inputCheckbox.checked) }) } function Login(parent) { // создаем и отрысовываем поле для ввода логина const inputLogin = document.createElement('input') inputLogin.type = 'text' inputLogin.placeholder = 'Insert login' parent.append(inputLogin) // создаем геттеры this.getValue = () => inputLogin.value // создаем сеттеры this.setValue = (value) => inputLogin.value = value // starting onChange inputLogin.addEventListener('input', () => { this.onChange(inputLogin.value) }) } // создание и отрисовывание формы для логина/пароля // const form = document.createElement('form') // если формируем форму, а не обычный див, тогда не работает запрос и появляется в адресной строке "?" после домена в урле const form = document.createElement('div') parent.append(form) const loginLabel = document.createElement('label') loginLabel.innerText = 'Login:' form.append(loginLabel) let breakSymbol = document.createElement('br') form.append(breakSymbol) // отрисовываем поле логина const login = new Login(form) breakSymbol = document.createElement('br') form.append(breakSymbol) const passwordLabel = document.createElement('label') passwordLabel.innerText = 'Password:' form.append(passwordLabel) breakSymbol = document.createElement('br') form.append(breakSymbol) // отрисовываем поле для пароля const password = new Password(form, true) breakSymbol = document.createElement('br') form.append(breakSymbol) // создание и отрисовывание кнопки для входа const confirmBtn = document.createElement('button') confirmBtn.innerText = 'Confirm' confirmBtn.id = 'confirmBtn' confirmBtn.type = 'submit' confirmBtn.style.marginTop = '10px' confirmBtn.disabled = true form.append(confirmBtn) // change confirmBtn.disabled function checkButton() { if (login.getValue() !== '' && password.getValue() !== '') { confirmBtn.disabled = false } else { confirmBtn.disabled = true } return confirmBtn.disabled } checkButton() // listening password and login login.onChange = password.onChange = checkButton // create getters this.getPasswordValue = () => password.getValue() this.getPasswordOpen = () => password.getOpen() this.getLoginValue = () => login.getValue() this.getButtonStatus = () => confirmBtn.disabled // create setters this.setPasswordValue = (value) => password.setValue(value) this.setLoginValue = (value) => login.setValue(value) this.setPasswordOpen = (status) => password.setOpen(status) // create callbacks this.onOpenChange = open => password.onOpenChange = open } // проверяем, что сейчас находится в урле и, исходя из приставки в урле, запускаем нужную функцию window.onhashchange = () => { const [, route, _id] = location.hash.split('/') const routes = { category() { store.dispatch(actionCategoryFindOne(_id)) }, good() { store.dispatch(actionGoodFindOne(_id)) }, login() { if (!localStorage.authToken) { // если пользователь залогинен, то ему больше не рисовать форму логина main.innerHTML = 'Для просмотра корзины Вам необходимо авторизоваться!
Авторизация` } } } if (route in routes) { routes[route]() } } window.onhashchange() // 3. разобраться с тем, почему корзина хуйней страдает // 6. сonst form = document.createElement('form') // если формируем форму, а не обычный див на логине, тогда не работает запрос и появляется в адресной строке "?" после домена в урле