const API_URL = 'http://shop-roles.node.ed.asmer.org.ua/graphql'; function getGql(url) { return (query, variables = {}) => { const token = store.getState().auth.token; return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: token ? 'Bearer ' + token : '', }, body: JSON.stringify({ query, variables, }), }) .then((response) => response.json()) .then((response) => { if (response.data) { return Object.values(response.data)[0]; } else if (response.errors) { throw new Error(JSON.stringify(response.errors)); } }); }; } const gql = getGql(API_URL); const gqlCreateOrder = (orderGoods) => { const orderQuery = `mutation ordering($orderGoods: OrderInput) { OrderUpsert(order: $orderGoods) { _id, total, orderGoods { good { name } } } }`; return gql(orderQuery, { orderGoods: { orderGoods } }); }; const gqlGetCategories = () => { const categoriesQuery = `query categories($q: String){ CategoryFind(query: $q){ _id name, goods{ name }, parent{ name }, image{ url }, subCategories{ name, subCategories{ name } } } }`; return gql(categoriesQuery, { q: '[{"parent": null}]' }); }; const gqlGetCategory = (id) => { const categoryQuery = `query category($q: String) { CategoryFindOne(query: $q) { _id name, goods{ name, _id, images{ _id, url }, price }, parent { _id, name }, subCategories{ name, _id subCategories{ name, _id } } } }`; return gql(categoryQuery, { q: `[{"_id": "${id}"}]` }); }; const gqlGetGood = (id) => { const goodQuery = `query good($q: String) { GoodFindOne(query: $q) { _id, name, categories{ _id, name }, description, price, images{ _id, url } } }`; return gql(goodQuery, { q: `[{"_id": "${id}"}]` }); }; const gqlLogin = (login, password) => { const loginQuery = `query login($login:String, $password:String){ login(login:$login, password:$password) }`; return gql(loginQuery, { login, password }); }; const gqlCreateUser = (login, password) => { const registrationQuery = `mutation registration($login:String, $password: String){ UserUpsert(user: {login:$login, password: $password}){ _id login createdAt } }`; return gql(registrationQuery, { login, password }); }; const gqlGetOwnerOrders = () => { const ordersQuery = `query orders($q: String) { OrderFind(query: $q) { _id, total, owner{ _id, login }, orderGoods{ price, count, good{ name } } } }`; return gql(ordersQuery, { q: `[{}]` }); }; function createStore(reducer) { let state = reducer(undefined, {}); let cbs = []; const getState = () => state; const subscribe = (cb) => (cbs.push(cb), () => (cbs = cbs.filter((c) => c !== cb))); const dispatch = (action) => { if (typeof action === 'function') { return action(dispatch, getState); } const newState = reducer(state, action); if (newState !== state) { state = newState; for (let cb of cbs) cb(state); } }; return { getState, dispatch, 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); function promiseReducer(state = {}, { key, type, status, payload, error }) { if (type === 'PROMISE') { return { ...state, [key]: { status, payload, error } }; } return state; } const actionPromise = (key, promise) => async (dispatch) => { dispatch(actionPending(key)); try { const payload = await promise; dispatch(actionFulfilled(key, payload)); return payload; } catch (error) { dispatch(actionRejected(key, error)); main.innerHTML = '
Error
'; } }; function authReducer(state = {}, { type, token }) { if (type === 'AUTH_LOGIN') { try { let mediumStr = token.split('.')[1]; let result = JSON.parse(atob(mediumStr)); return { ...state, token: token, payload: result }; } catch (e) { return {}; } } if (type === 'AUTH_LOGOUT') { return {}; } return state; } function cartReducer(state = {}, { type, good, count }) { let goodKey, oldCount, goodValue; if (good) { goodKey = good['_id']; oldCount = state[goodKey]?.count || 0; goodValue = { good, count: oldCount }; } if (type === 'CART_ADD') { goodValue.count += +count; return { ...state, [goodKey]: goodValue }; } else if (type === 'CART_SUB') { goodValue.count -= +count; if (goodValue.count <= 0) { delete state[goodKey]; return { ...state }; } return { ...state, [goodKey]: goodValue }; } else if (type === 'CART_DEL') { delete state[goodKey]; return { ...state }; } else if (type === 'CART_SET') { goodValue.count = +count; if (goodValue.count <= 0) { delete state[goodKey]; return { ...state }; } return { ...state, [goodKey]: goodValue }; } else if (type === 'CART_CLEAR') { return {}; } return state; } const actionPending = (key) => ({ type: 'PROMISE', status: 'PENDING', key }); const actionFulfilled = (key, payload) => ({ type: 'PROMISE', status: 'FULFILLED', key, payload }); const actionRejected = (key, error) => ({ type: 'PROMISE', status: 'REJECTED', key, error }); const actionAuthLogin = (token) => ({ type: 'AUTH_LOGIN', token }); const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' }); const actionGetCategories = () => actionPromise('categories', gqlGetCategories()); const actionGetCategoryById = (id) => actionPromise('category', gqlGetCategory(id)); const actionGoodById = (id) => actionPromise('good', gqlGetGood(id)); const actionLogin = (login, password) => actionPromise('login', gqlLogin(login, password)); const actionCreateUser = (login, password) => actionPromise('register', gqlCreateUser(login, password)); const actionOwnerOrders = () => actionPromise('history', gqlGetOwnerOrders()); const actionCreateOrder = (orderGoods) => actionPromise('cart', gqlCreateOrder(orderGoods)); const actionFullLogin = (login, password) => async (dispatch) => { const token = await dispatch(actionLogin(login, password)); if (typeof token === 'string') { dispatch(actionAuthLogin(token)); } }; const actionFullRegister = (login, password) => async (dispatch) => { await dispatch(actionCreateUser(login, password)); dispatch(actionFullLogin(login, password)); }; const actionOrder = (orderGoods) => async (dispatch) => { await dispatch(actionCreateOrder(orderGoods)); console.log('order was created'); dispatch(actionCartClear()); }; 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' }); function localStoredReducer(originalReducer, localStorageKey) { function wrapper(state, action) { if (state === undefined) { try { let savedState = JSON.parse(localStorage[localStorageKey]); return savedState; } catch (e) {} } let newState = originalReducer(state, action); localStorage.setItem(localStorageKey, JSON.stringify(newState)); return newState; } return wrapper; } const store = createStore(localStoredReducer(totalReducer, 'total')); store.subscribe(() => console.log(store.getState())); function drawTitle(name) { let nameEl = document.createElement('div'); main.append(nameEl); nameEl.innerText = name; nameEl.classList.add('title'); } function drawCategoriesSection(categories, categoryEl) { let containerEl = document.createElement('section'); main.append(containerEl); containerEl.innerText = categoryEl; containerEl.classList.add('info-about-category'); for (const category of categories) { let categoryName = document.createElement('a'); categoryName.classList.add('link-style'); containerEl.append(categoryName); categoryName.href = `#/category/${category._id}`; categoryName.innerText = category.name; } } function drawImage(parent, url, route, id) { let imageContainer = document.createElement('div'); parent.append(imageContainer); if (route == 'category') { imageContainer.addEventListener('click', (e) => (location.href = `#/good/${id}`)); } imageContainer.classList.add('image-container'); let goodImage = document.createElement('img'); imageContainer.append(goodImage); goodImage.classList.add('good-image'); goodImage.src = 'http://shop-roles.node.ed.asmer.org.ua/' + url; } function drawSum(parent, priceText) { let sumEl = document.createElement('div'); parent.append(sumEl); sumEl.classList.add('price'); sumEl.innerText = priceText; } function drawCounter(parent, value) { let counterEl = document.createElement('input'); parent.append(counterEl); counterEl.type = 'number'; counterEl.min = 1; counterEl.value = value; return counterEl; } function drawAddButton(parent) { let cartAddButton = document.createElement('button'); parent.append(cartAddButton); cartAddButton.innerText = 'Add to cart'; cartAddButton.classList.add('button'); return cartAddButton; } function drawDelButton(parent) { let cartDelButton = document.createElement('button'); parent.append(cartDelButton); cartDelButton.innerText = 'Delete from cart'; cartDelButton.classList.add('button'); return cartDelButton; } const drawCategory = () => { const [, route] = location.hash.split('/'); if (route !== 'category') return; let status, payload; if (store.getState().promise.category) { ({ status, payload, error } = store.getState().promise.category); } if (status === 'PENDING') { main.innerHTML = ``; } if (status === 'FULFILLED') { main.innerHTML = ''; const { name, subCategories, parent, goods } = payload; drawTitle(name); if (subCategories?.length > 0) { drawCategoriesSection(subCategories, 'Subcategories: '); } if (parent) { drawCategoriesSection([parent], 'Parent category: '); } if (goods?.length > 0) { let goodsContainer = document.createElement('section'); main.append(goodsContainer); goodsContainer.classList.add('goods-container'); for (const good of goods) { let goodCart = document.createElement('div'); goodsContainer.append(goodCart); goodCart.classList.add('good-cart'); let goodName = document.createElement('a'); goodCart.append(goodName); goodName.href = `#/good/${good._id}`; goodName.innerText = good.name; if (good.images?.length > 0) { drawImage(goodCart, good.images[0].url, route, good._id); } if (good.price) { drawSum(goodCart, `${good.price} UAH`); } let goodCountEl = drawCounter(goodCart, 1); let cartAddButton = drawAddButton(goodCart); let cartDelButton = drawDelButton(goodCart); cartAddButton.addEventListener('click', () => { store.dispatch(actionCartAdd(good, goodCountEl.value)); }); cartDelButton.addEventListener('click', () => { store.dispatch(actionCartDel(good)); }); } } else { main.innerHTML += `
Goods out of stock
`; } } }; store.subscribe(() => drawCategory()); function LoginForm(parent) { parent.innerHTML = ''; if (location.hash == '#/login') { let loginTitle = document.createElement('div'); loginTitle.classList.add('title'); loginTitle.innerText = 'Account login'; parent.append(loginTitle); } else if (location.hash == '#/register') { let registerTitle = document.createElement('div'); registerTitle.classList.add('title'); registerTitle.innerText = 'Registration'; parent.append(registerTitle); } let form = document.createElement('form'); form.classList.add('form'); let loginLabel = document.createElement('label'); loginLabel.innerText = 'Login: '; form.append(loginLabel); let login = new Login(form); let passwordLabel = document.createElement('label'); passwordLabel.innerText = 'Password: '; form.append(passwordLabel); let password = new Password(form, false); let submit = document.createElement('button'); submit.classList.add('button'); submit.innerText = 'Submit'; form.append(submit); parent.append(form); this.validateForm = () => { if (login.getValue() == '' || password.getValue() == '') { submit.disabled = true; } else { submit.disabled = false; if (this.onValidForm) { this.onValidForm(); } } }; submit.addEventListener('click', (e) => { e.preventDefault(); if (this.onSubmitForm) { this.onSubmitForm(); } }); this.getLoginValue = () => { return login.getValue(); }; this.setLoginValue = (value) => { login.setValue(value); }; this.getPasswordValue = () => { return password.getValue(); }; this.setPasswordValue = (value) => { password.setValue(value); }; this.validateForm(); login.onChange = this.validateForm; password.onChange = this.validateForm; } function Login(parent) { let loginInputEl = document.createElement('input'); loginInputEl.type = 'text'; parent.append(loginInputEl); loginInputEl.addEventListener('input', () => { if (this.onChange) { this.onChange(); } }); this.setValue = (value) => { loginInputEl.value = value; }; this.getValue = () => { return loginInputEl.value; }; } function Password(parent, isChecked) { let passInputEl = document.createElement('input'); parent.append(passInputEl); let passVisibilityCheckbox = document.createElement('input'); passVisibilityCheckbox.type = 'checkbox'; passVisibilityCheckbox.checked = isChecked; let passVisibilityLabel = document.createElement('label'); passVisibilityLabel.textContent = 'Show password'; passVisibilityLabel.append(passVisibilityCheckbox); parent.append(passVisibilityLabel); if (isChecked) { passInputEl.type = 'text'; } else { passInputEl.type = 'password'; } passVisibilityCheckbox.addEventListener('change', (event) => { if (event.currentTarget.checked) { passInputEl.type = 'text'; } else { passInputEl.type = 'password'; } if (this.onOpenChange) { this.onOpenChange(event.currentTarget.checked); } }); passInputEl.addEventListener('input', () => { if (this.onChange) { this.onChange(); } }); this.setValue = (value) => { passInputEl.value = value; }; this.getValue = () => { return passInputEl.value; }; this.setOpen = (value) => { passVisibilityCheckbox.checked = value; }; this.getOpen = () => { return passVisibilityCheckbox.checked; }; } function drawLoginForm() { const form = new LoginForm(main); form.onSubmitForm = () => store.dispatch(actionFullLogin(form.getLoginValue(), form.getPasswordValue())); } store.subscribe(() => { const [, route] = location.hash.split('/'); if (route !== 'login') return; let status, payload; if (store.getState().promise?.login) { ({ status, payload, error } = store.getState().promise?.login); } if (status === 'FULFILLED') { if (payload) { location.hash = ''; main.innerHTML = ''; } else { let errorMessageEl = document.createElement('div'); main.append(errorMessageEl); errorMessageEl.innerText = 'You entered wrong login or password'; } } }); store.subscribe(() => { userName.innerText = 'Hello, ' + (store.getState().auth.payload?.sub?.login || 'anonymous user 👋'); login.hidden = store.getState().auth.token; registration.hidden = store.getState().auth.token; logout.hidden = !store.getState().auth.token; historyPage.hidden = !store.getState().auth.token; }); function drawRegisterForm() { const form = new LoginForm(main); form.onSubmitForm = () => store.dispatch(actionFullRegister(form.getLoginValue(), form.getPasswordValue())); } const drawGood = () => { const [, route] = location.hash.split('/'); if (route !== 'good') return; let status, payload; if (store.getState().promise.good) { ({ status, payload, error } = store.getState().promise.good); } if (status === 'PENDING') { main.innerHTML = ``; } if (status === 'FULFILLED') { main.innerHTML = ''; const { name, images, categories, price, description } = payload; drawTitle(name); if (categories?.length > 0) { drawCategoriesSection(categories, 'Category: '); } let goodSection = document.createElement('section'); main.append(goodSection); goodSection.classList.add('good-section'); let imagesContainer = document.createElement('div'); goodSection.append(imagesContainer); if (images.length > 0) { for (const image of images) { drawImage(imagesContainer, image.url); } } let goodInfo = document.createElement('div'); goodSection.append(goodInfo); goodInfo.classList.add('good-info'); if (description) { let descriptionEl = document.createElement('div'); descriptionEl.innerText = description; descriptionEl.classList.add('description'); goodInfo.append(descriptionEl); } if (price) { drawSum(goodInfo, `${price} UAH`); } let goodCountEl = drawCounter(goodInfo, 1); let buttonsContainer = document.createElement('div'); goodInfo.append(buttonsContainer); buttonsContainer.classList.add('buttons-container'); let cartAddButton = drawAddButton(buttonsContainer); let cartDelButton = drawDelButton(buttonsContainer); cartAddButton.addEventListener('click', () => { store.dispatch(actionCartAdd(payload, goodCountEl.value)); }); cartDelButton.addEventListener('click', () => { store.dispatch(actionCartDel(payload)); }); } }; store.subscribe(() => drawGood()); function drawGoodsOrderContainer(parent, orderNameValue) { let goodsOrderContainer = document.createElement('div'); goodsOrderContainer.classList.add('order-container'); let orderName = document.createElement('div'); parent.append(goodsOrderContainer); goodsOrderContainer.append(orderName); orderName.innerText = orderNameValue; return goodsOrderContainer; } function drawTotalAmount(parent) { let totalAmountEl = document.createElement('div'); parent.append(totalAmountEl); totalAmountEl.classList.add('price'); totalAmountEl.classList.add('total-amount'); return totalAmountEl; } const drawOrderHistory = () => { const [, route] = location.hash.split('/'); if (route !== 'history') return; let status, payload; if (store.getState().promise.history) { ({ status, payload, error } = store.getState().promise.history); } if (status === 'PENDING') { main.innerHTML = ``; } if (status === 'FULFILLED') { main.innerHTML = ''; drawTitle('My orders'); payload = payload.filter((order) => order.orderGoods?.every((item) => item)).reverse(); payload.forEach((order) => { let finalSum = 0; const { orderGoods, _id, total } = order; if (orderGoods.length > 0) { let orderContainer = drawGoodsOrderContainer(main, `Order: ${_id}`); for (orderGood of orderGoods) { if (orderGood?.good?.name && orderGood?.count && orderGood?.price) { let orderGoodName = document.createElement('div'); orderContainer.append(orderGoodName); orderGoodName.innerText = `Good: ${orderGood?.good?.name}`; let goodCount = document.createElement('div'); orderContainer.append(goodCount); goodCount.innerText = `Count: ${orderGood?.count}`; drawSum(orderContainer, `${orderGood?.price} UAH`); } if (orderGood?.count && orderGood?.price) { finalSum += orderGood?.count * orderGood?.price; } } let totalAmountEl = drawTotalAmount(orderContainer); if (total > 0) { totalAmountEl.innerText += `Total amount: ${total} UAH`; } else { totalAmountEl.innerText += `Total amount: ${finalSum} UAH`; } } }); } }; store.subscribe(() => drawOrderHistory()); const drawCartPage = () => { const [, route] = location.hash.split('/'); if (route !== 'cart') return; if (!(Object.values(store.getState().cart).length > 0)) { main.innerHTML = '
Cart is empty
'; } else { main.innerHTML = ''; let totalAmount = 0; let orderArray = []; drawTitle('Cart'); for (const goodOnCart of Object.values(store.getState().cart)) { const { good, count } = goodOnCart; let goodContainer = drawGoodsOrderContainer(main, `${good.name} (${good._id})`); drawSum(goodContainer, `${good.price} UAH`); let countContainer = document.createElement('div'); goodContainer.append(countContainer); countContainer.classList.add('count-container'); let countText = document.createElement('div'); countContainer.append(countText); countText.innerText = 'Count: '; let decreaseCountEl = document.createElement('button'); countContainer.append(decreaseCountEl); decreaseCountEl.classList.add('icon-container'); let decreaseImg = document.createElement('img'); decreaseCountEl.append(decreaseImg); decreaseImg.src = './img/minus.png'; let goodCountEl = drawCounter(countContainer, count); let increaseCountEl = document.createElement('button'); countContainer.append(increaseCountEl); increaseCountEl.classList.add('icon-container'); let increaseImg = document.createElement('img'); increaseCountEl.append(increaseImg); increaseImg.src = './img/plus.png'; drawSum(goodContainer, `Total: ${count * good.price} UAH`); totalAmount += count * good.price; goodCountEl.addEventListener('change', (e) => { store.dispatch(actionCartSet(good, goodCountEl.value)); }); decreaseCountEl.addEventListener('click', (e) => { store.dispatch(actionCartSub(good)); }); increaseCountEl.addEventListener('click', (e) => { store.dispatch(actionCartAdd(good)); }); } orderArray = Object.values(store.getState().cart).map((value) => { return { good: { _id: value.good._id }, count: value.count, }; }); let totalAmountEl = drawTotalAmount(main); totalAmountEl.innerText = `Total sum: ${totalAmount} UAH`; let createOrderButton = document.createElement('button'); main.append(createOrderButton); createOrderButton.classList.add('button'); createOrderButton.classList.add('order-button'); createOrderButton.innerText = 'Checkout'; let warningMessage = document.createElement('div'); main.append(warningMessage); warningMessage.classList.add('warning-message'); warningMessage.innerText = 'You need to login or register to place an order'; if (store.getState().auth.token) { createOrderButton.addEventListener('click', (e) => { store.dispatch(actionOrder(orderArray)); }); } else { createOrderButton.addEventListener('click', (e) => { warningMessage.style.display = 'block'; }); } } }; let cartIcon = document.querySelector('img.cart-icon'); cartIcon.addEventListener('click', () => (location.href = `#/cart`)); store.subscribe(() => drawCartPage()); store.subscribe(() => { let sumTotal = 0; for (let { count } of Object.values(store.getState().cart)) { sumTotal += count; } cartIconEl.innerText = sumTotal; }); store.dispatch(actionGetCategories()); store.subscribe(() => { const { status, payload } = store.getState().promise.categories; if (status === 'FULFILLED' && payload) { aside.innerHTML = ''; for (const { _id, name } of payload) { aside.innerHTML += `${name}`; } } }); store.subscribe(() => { const [, route] = location.hash.split('/'); if (!route) { main.innerHTML = ``; } }); logout.addEventListener('click', () => { store.dispatch(actionAuthLogout()); store.dispatch(actionCartClear()); }); window.onhashchange = () => { const [, route, _id] = location.hash.split('/'); const routes = { category() { store.dispatch(actionGetCategoryById(_id)); }, good() { store.dispatch(actionGoodById(_id)); console.log('good', _id); }, login() { drawLoginForm(); }, register() { drawRegisterForm(); }, history() { store.dispatch(actionOwnerOrders()); }, cart() { drawCartPage(); }, }; if (route in routes) { routes[route](); } }; window.onhashchange();