// 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 } } // ---------------------------------------------------------- // decode token function jwtDecode(token) { try { return JSON.parse(atob(token.split('.')[1])) } catch (e) { } } // ---------------------------------------------------------- // reducers function authReducer(state = {}, { type, token }) { //authorise reducer if (state === undefined) { if (localStorage.authToken) { type = "AUTH_LOGIN"; token = localStorage.authToken; } } if (type === 'AUTH_LOGIN') { //то мы логинимся const payload = jwtDecode(token) if (payload) { return { token, payload } } } if (type === 'AUTH_LOGOUT') { //мы разлогиниваемся return {} } return state } function promiseReducer(state = {}, { type, name, status, payload, error }) { //promise reducer if (type === 'PROMISE') { return { ...state, [name]: { status, payload, error } } } return state } function cartReducer(state = {}, { type, good, count = 1 }) { if (type === 'CART_ADD') { return { ...state, [good._id]: { count: (state[good._id]?.count || 0) + count, good: good } } } if (type === 'CART_CHANGE') { return { ...state, [good._id]: { count, good } } } if (type === 'CART_DELETE') { delete state[good._id] return { ...state, } } if (type === 'CART_CLEAR') { return {} } return state } const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count }) const actionCartChange = (good, count = 1) => ({ type: 'CART_CHANGE', good, count }) const actionCartDelete = (good) => ({ type: 'CART_DELETE', good }) const actionCartClear = () => ({ type: 'CART_CLEAR' }) function combineReducers(reducers) { function combinedReducer(combinedState = {}, action) { const newCombinedState = {} for (const [reducerName, reducer] of Object.entries(reducers)) { const newSubState = reducer(combinedState[reducerName], action) if (newSubState !== combinedState[reducerName]) { newCombinedState[reducerName] = newSubState } } if (Object.keys(newCombinedState).length === 0) { return combinedState } return { ...combinedState, ...newCombinedState } } return combinedReducer } const store = createStore(combineReducers({ promise: promiseReducer, auth: authReducer, cart: cartReducer })) // store.subscribe(() => console.log(store.getState())) // ---------------------------------------------------------- // GQL const getGQL = url => (query, variables) => fetch(url, { method: 'POST', headers: { "Content-Type": "application/json", ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } : {}) }, body: JSON.stringify({ query, variables }) }).then(res => res.json()) .then(data => { if (data.data) { return Object.values(data.data)[0] } else throw new Error(JSON.stringify(data.errors)) }) const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/' const gql = getGQL(backendURL + 'graphql') // ---------------------------------------------------------- // promises const actionPromise = (name, promise) => async dispatch => { dispatch(actionPending(name)) try { let payload = await promise dispatch(actionFulfilled(name, payload)) return payload } catch (e) { dispatch(actionRejected(name, e)) } } 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 }) // ---------------------------------------------------------- // actions const actionFullRegister = (login, password) => actionPromise('fullRegister', gql(`mutation UserUpsert($login: String, $password: String){UserUpsert(user: {login:$login,password:$password}){_id}}`, { login: login, password: password })) const actionAuthLogin = (token) => (dispatch, getState) => { const oldState = getState() dispatch({ type: 'AUTH_LOGIN', token }) const newState = getState() if (oldState !== newState) localStorage.authToken = token } const actionAuthLogout = () => dispatch => { dispatch({ type: 'AUTH_LOGOUT' }) localStorage.removeItem('authToken') } const actionFullLogin = (login, password) => actionPromise('fullLogin', gql(`query login($login:String,$password:String){login(login:$login,password:$password)}`, { login: login, password: password })) const orderFind = () => actionPromise('orderFind', gql(`query orderFind{ OrderFind(query: "[{}]"){ _id createdAt total orderGoods {_id price count good{name price images{url}}} } }`, { q: JSON.stringify([{}]) })) const actionAddOrder = (cart) => actionPromise('actionAddOrder', gql(`mutation newOrder($cart: [OrderGoodInput]) {OrderUpsert(order: {orderGoods: $cart}) {_id total}}`, { cart: cart })) const actionRootCats = () => actionPromise('rootCats', gql(`query { CategoryFind(query: "[{\\"parent\\":null}]"){ _id name } }`)) const actionCatById = (_id) => // √ actionPromise('catById', gql(`query catById($q: String){ CategoryFindOne(query: $q){ _id name goods { _id name price images { url } } subCategories{_id name} } }`, { q: JSON.stringify([{ _id }]) })) const actionGoodById = (_id) => actionPromise('goodById', gql(`query goodById($q: String){ GoodFindOne(query: $q){ _id name price description images { url } } }`, { q: JSON.stringify([{ _id }]) })) store.dispatch(actionRootCats()) // ---------------------------------------------------------- // subscribe store.subscribe(() => { const { rootCats } = (store.getState()).promise if (rootCats?.payload) { cat.innerHTML = `
  • Categories
  • ` for (const { _id, name } of rootCats?.payload) { const categories = document.createElement('li') categories.innerHTML = `${name}` categories.style = ' padding-left: 30px ; ' cat.append(categories) } } else if (!rootCats) { cat.innerHTML = '' } }) store.subscribe(() => { const { catById } = (store.getState()).promise const [, route, _id] = location.hash.split('/') if (catById?.payload && route === 'category') { main.innerHTML = `` const { name } = catById.payload const card = document.createElement('div') card.id = 'sub-card' card.innerHTML = `

    ${name}


    ` const backPart = document.createElement('div') backPart.id = 'back' const backBtn = document.createElement('button') backBtn.setAttribute('type', 'button'); backBtn.addEventListener('click', () => { history.back(); }); backBtn.innerText = '⬅' backPart.appendChild(backBtn) if (catById.payload.subCategories) { for (const { _id, name } of catById.payload?.subCategories) { card.innerHTML += `${name}` } } // card.append(backPart) main.append(card, backPart) for (const { _id, name, price, images } of catById.payload?.goods) { const card = document.createElement('div') card.id = 'card' card.innerHTML = `
    ${name}

    Price: $${price}



    More details ->

    ` let button = document.createElement('button') button.innerText = 'BUY' button.className = 'buy-btn' button.setAttribute('type', 'button'); button.onclick = async () => { await store.dispatch(actionCartAdd({ _id: _id, name: name, price: price, images: images })) console.log('hi') } card.append(button) main.append(card) } } }) store.subscribe(() => { const { goodById } = (store.getState()).promise const [, route, _id] = location.hash.split('/') if (goodById?.payload && route === 'good') { const { _id, name, description, images, price } = goodById.payload main.innerHTML = `

    ${name}

    ` const card = document.createElement('div') card.id = 'desc-card' const backPart = document.createElement('div') backPart.id = 'back' const backBtn = document.createElement('button') backBtn.setAttribute('type', 'button'); backBtn.addEventListener('click', () => { history.back(); }); backBtn.innerText = '⬅' backPart.appendChild(backBtn) let block = document.createElement('div') block.id = 'price' block.innerHTML = `

    Price: $${price}

    ` let button = document.createElement('button') button.innerText = 'BUY' button.className = 'buy-btn' button.setAttribute('type', 'button'); button.style = 'height:80px' button.onclick = async () => { await store.dispatch(actionCartAdd({ _id: _id, name: name, price: price, images: images })) console.log('hi') } card.innerHTML = `

    ` card.append(block) card.innerHTML += `

    Description: ${description}

    ` main.append(backPart, card, button) } }) store.subscribe(() => { const { orderFind } = (store.getState()).promise const [, route, _id] = location.hash.split('/') if (orderFind?.payload && route === 'orderFind') { main.innerHTML = '

    ORDER HISTORY

    ' for (const { _id, createdAt, total, orderGoods } of orderFind.payload.reverse()) { const card = document.createElement('div') card.className = 'order-card' card.innerHTML = `

    Order: ${createdAt}

    ` for (const { count, good } of orderGoods) { const divGood = document.createElement('div') divGood.style = "display:flex;margin-bottom: 20px;" divGood.innerHTML += `
    ${good.name}
    Price: $${good.price}
    Amount: ${count} pt


    ` card.append(divGood) } card.innerHTML += 'Date: ' + new Date(+createdAt).toLocaleString().replace(/\//g, '.') + '' card.innerHTML += `

    Total: $${total}

    ` main.append(card) } } }) // ---------------------------------------------------------- // window function display() { let token = localStorage.authToken if (token) { form_yes.style.display = 'flex' form_no.style.display = 'none' UserNick.innerText = JSON.parse(window.atob(localStorage.authToken.split('.')[1])).sub.login } else { form_yes.style.display = 'none' form_no.style.display = 'flex' } } display() window.onhashchange = () => { const [, route, _id] = location.hash.split('/') const routes = { category() { store.dispatch(actionCatById(_id)) }, good() { store.dispatch(actionGoodById(_id)) }, login() { main.innerHTML = '' let form = document.createElement('div') let div1 = document.createElement('div') let div2 = document.createElement('div') div1.innerHTML = `

    LOGIN

    ` let loginInput = document.createElement('input') loginInput.placeholder = 'Type your username' loginInput.name = 'login' div1.style.display = 'flex' div1.style.flexDirection = 'column' let passwordInput = document.createElement('input') passwordInput.placeholder = 'Type your password' passwordInput.name = 'password' passwordInput.type = 'password'; div1.append(loginInput) div2.append(passwordInput) let button = document.createElement('button') button.innerText = "LOGIN" button.id = 'login-btn' button.setAttribute('type', 'button'); button.onclick = async () => { let tokenPromise = async () => await store.dispatch(actionFullLogin(loginInput.value, passwordInput.value)) let token = await tokenPromise() if (token !== null) { store.dispatch(actionAuthLogin(token)) display() document.location.href = "#/orderFind"; } else { loginInput.value = '' passwordInput.value = '' alert("Incorrect username or password.") store.dispatch(actionAuthLogout()) } } form.append(div1, div2, button) main.append(form) }, register() { main.innerHTML = '' let form = document.createElement('div') let div1 = document.createElement('div') let div2 = document.createElement('div') div1.innerHTML += `

    REGISTER

    ` let loginInput = document.createElement('input') loginInput.placeholder = "Type your username" div1.append(loginInput) let passwordInput = document.createElement('input') passwordInput.placeholder = "Type your password" passwordInput.type = 'password' div2.append(passwordInput) let button = document.createElement('button') button.innerText = "CREATE ACCOUNT" button.id = 'reg-btn' button.setAttribute('type', 'button'); let textAlert = document.createElement('div') let textAlert2 = document.createElement('div') let putInText = "Username and password required!" let userAlready = "An account with this username already exist!" textAlert.append(userAlready) textAlert2.append(putInText) textAlert2.style = 'display : none; color : red' textAlert.style = 'display : none; color : red' button.onclick = async () => { let register = await store.dispatch(actionFullRegister(loginInput.value, passwordInput.value)) let tokenPromise = async () => await store.dispatch(actionFullLogin(loginInput.value, passwordInput.value)) if (loginInput.value == '' || passwordInput.value == '') { textAlert2.style.display = 'block' } else { if (register !== null) { let token = await tokenPromise() store.dispatch(actionAuthLogin(token)) // console.log(token) display() document.location.href = "#/orderFind"; } else { textAlert.style.display = 'block' textAlert2.style.display = 'none' } } } form.append(div1, div2, button) form.append(textAlert, textAlert2) main.append(form) }, orderFind() { store.dispatch(orderFind()) }, cart() { main.innerHTML = '

    CART

    ' for (const [_id, obj] of Object.entries(store.getState().cart)) { const card = document.createElement('div') card.style = 'width: 33.33%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px;display: flex; flex-direction: column ; align-items: center ; justify-content: space-between' const { count, good } = obj card.innerHTML += `Products: ${good.name}

    Цена: $${good.price}

    ` const calculation = document.createElement('div') const buttonAdd = document.createElement('button') buttonAdd.innerHTML = '+' buttonAdd.setAttribute('type', 'button'); buttonAdd.onclick = async () => { inputCount.value = +inputCount.value + 1 await store.dispatch(actionCartChange({ _id: _id, name: good.name, price: good.price, images: good.images }, +inputCount.value)) cardTotal.innerHTML = `

    Total: $${goodPrice()}


    ` } calculation.append(buttonAdd) const inputCount = document.createElement('input') inputCount.value = +count inputCount.disabled = 'disabled' inputCount.className = 'inputCount' calculation.append(inputCount) const buttonLess = document.createElement('button') buttonLess.innerHTML = '-' buttonLess.setAttribute('type', 'button'); buttonLess.onclick = async () => { if ((+inputCount.value) > 1) { inputCount.value = +inputCount.value - 1 await store.dispatch(actionCartChange({ _id: _id, name: good.name, price: good.price, images: good.images }, +inputCount.value)) cardTotal.innerHTML = `

    Total: $${goodPrice()}


    ` } } calculation.append(buttonLess) const buttonDelete = document.createElement('button') buttonDelete.innerText = 'Delete' buttonDelete.className = 'buttonDelete' buttonDelete.setAttribute('type', 'button'); buttonDelete.onclick = async () => { await store.dispatch(actionCartDelete({ _id: _id, name: good.name, price: good.price, images: good.images })) card.style.display = 'none' cardTotal.innerHTML = `

    Total: $${goodPrice()}


    ` } card.append(calculation) card.append(buttonDelete) main.append(card) } const cardTotalDiv = document.createElement('div') cardTotalDiv.id = 'total' const cardTotal = document.createElement('div') cardTotal.innerHTML = `

    Total: $${goodPrice()}

    ` cardTotalDiv.append(cardTotal) let cartAlert = document.createElement('div') cartAlert.innerHTML = `

    Your cart seems empty 😟

    ` cartAlert.style.display = 'none' cartAlert.id = 'cart-alert' if (localStorage.authToken != '') { const button = document.createElement('button') button.innerHTML += 'ORDER' button.setAttribute('type', 'button'); if(goodPrice() != 0){ button.onclick = async () => { await store.dispatch(actionAddOrder(Object.entries(store.getState().cart).map(([_id, count]) => ({ count: count.count, good: { _id } })))); await store.dispatch(actionCartClear()); document.location.href = "#/orderFind"; } } else{ cartAlert.style.display = 'flex' } // button.className = 'btn btn-primary' cardTotalDiv.append(button) } main.append(cardTotalDiv, cartAlert) } } if (route in routes) { routes[route]() } } window.onhashchange() 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 } window.onhashchange() function goodPrice() { return Object.entries(store.getState().cart).map(i => x += i[1].count * i[1].good.price, x = 0).reverse()[0] || 0 }