// !store 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 в объект } } // ! decoding token function jwtDecode(token){ try { return JSON.parse(atob(token.split('.')[1])) } catch(e){ } } // ! reducers function authReducer(state, {type, token}){ if(state === undefined){ if(localStorage.authToken){ token = localStorage.authToken type = 'AUTH_LOGIN' } else{ type = 'AUTH_LOGOUT' } } if(type === 'AUTH_LOGIN'){ state = jwtDecode(token) localStorage.authToken = token } if(type === 'AUTH_LOGOUT'){ localStorage.authToken = '' state = {} } return state || {} } function promiseReducer(state={}, {type, name, status, payload, error}){ 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){ //пачку редьюсеров как объект {auth: authReducer, promise: promiseReducer} function combinedReducer(combinedState={}, action){ //combinedState - типа {auth: {...}, promise: {....}} 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 //нам возвращают один редьюсер, который имеет стейт вида {auth: {...стейт authReducer-а}, promise: {...стейт promiseReducer-а}} } 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/graphql' const gql = getGQL(backendURL + '/graphql') // !PROMISE const actionPromise = (name, promise) => async dispatch => { dispatch(actionPending(name)) try { let payload = await promise dispatch(actionFulfilled(name, payload)) return payload } catch(error){ dispatch(actionRejected(name, error)) } } const actionPending = name => ({type:'PROMISE',name, status: 'PENDING'}) const actionFulfilled = (name,payload) => ({type:'PROMISE',name, status: 'FULFILLED', payload}) const actionRejected = (name,error) => ({type:'PROMISE',name, status: 'REJECTED', 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 actionFullLogin = (login, password) => //вход actionPromise('fullLogin', gql(`query login($login:String,$password:String){login(login:$login,password:$password)}`, {login: login, password:password})) const actionAuthLogout = () => dispatch => { dispatch({type: 'AUTH_LOGOUT'}) localStorage.removeItem('authToken') } 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){ aside.innerHTML = `
  • Категории
  • ` for (const {_id, name} of rootCats?.payload){ const categories = document.createElement('li') categories.innerHTML = `${name}` categories.style = ' padding-left: 30px ; ' aside.append(categories) } } }) 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.style = 'height: auto;width: 100%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px;' card.innerHTML = `

    ${name}


    ` if(catById.payload.subCategories){ for (const {_id, name} of catById.payload?.subCategories){ card.innerHTML +=`${name}` } } main.append(card) for (const {_id, name, price, images} of catById.payload?.goods){ const card = document.createElement('div') card.style = 'height: auto;width: 30%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px; display: flex ; flex-direction: column ; justify-content: space-between' card.innerHTML = `
    ${name}

    Цена: ${price} грн.

    Подробнее

    ` let button = document.createElement('button') button.innerText = 'Купить' button.className = 'btn-buy' button.style = 'width: 100%; font-family: Impact; letter-spacing : 1px' button.onclick = async () => { await store.dispatch(actionCartAdd({_id: _id, name: name, price: price, images: images})) console.log('tap') } 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 {name,description,images,price} = goodById.payload main.innerHTML = `

    ${name}

    ` const card = document.createElement('div') card.innerHTML = `
    Цена: ${price} грн.

    Описание: ${description}

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

    История заказов

    ' for (const {_id, createdAt, total,orderGoods} of orderFind.payload.reverse()){ const card = document.createElement('div') card.style = 'width: 100%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px;' card.innerHTML = `

    Заказ: ${createdAt}

    ` for (const {count, good} of orderGoods){2 const divGood = document.createElement('div') divGood.style= "display:flex;margin-bottom: 20px;" divGood.innerHTML += `
    Товар: ${good.name}
    Цена: ${good.price} грн.
    Количество: ${count} шт.


    ` card.append(divGood) } card.innerHTML += 'Дата: '+new Date(+createdAt).toLocaleString().replace(/\//g, '.')+'' card.innerHTML += `
    Всего: ${total} грн.` main.append(card) } } }) // !WINDOW function display(){ let token = localStorage.authToken if(token){ form_yes.style.display = 'block' 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 = 'block' } } display() window.onhashchange = () => { const [, route, _id] = location.hash.split('/') mainContainer.scrollTo(0,0); const routes = { category(){ store.dispatch(actionCatById(_id)) }, good(){ store.dispatch(actionGoodById(_id)) }, login(){ main.innerHTML = '' let form = document.createElement('div') let div = document.createElement('div') div.innerHTML += `

    Вход

    ` let inputLogin = document.createElement('input') inputLogin.placeholder="Login" inputLogin.name = "login" div.append(inputLogin) form.append(div) let div2 = document.createElement('div') div.style.display = 'flex' div.style.flexDirection = 'column' let inputPassword = document.createElement('input') inputPassword.placeholder = "Password" inputPassword.name = "password" div2.append(inputPassword) form.append(div2) let button = document.createElement('button') button.innerText="Войти" button.style.padding = '15px 35px' button.style.marginTop = '20px' button.style.backgroundColor = 'yellowgreen' button.style.textTransform = 'uppercase' button.style.fontFamily = 'Impact' button.style.fontSize = '15px' button.onclick = async () => { let tokenPromise = async () => await store.dispatch(actionFullLogin(inputLogin.value, inputPassword.value)) let token = await tokenPromise() if(token!==null){ store.dispatch(actionAuthLogin(token)) console.log(token) display() document.location.href = "#/orderFind"; } else{ inputLogin.value = '' inputPassword.value = '' alert("Введен неверный логин или пароль !") store.dispatch(actionAuthLogout()) } } form.append(button) main.append(form) }, register(){ main.innerHTML = '' let form = document.createElement('div') let div = document.createElement('div') div.innerHTML += `

    Регистрация

    ` let inputLogin = document.createElement('input') inputLogin.placeholder="Login" div.append(inputLogin) form.append(div) let div2 = document.createElement('div') let inputPassword = document.createElement('input') inputPassword.placeholder="Password" div2.append(inputPassword) form.append(div2) let button = document.createElement('button') button.innerText="Зарегистрироваться" button.style.padding = '15px 35px' button.style.marginTop = '20px' button.style.backgroundColor = 'yellowgreen' button.style.textTransform = 'uppercase' button.style.fontFamily = 'Impact' button.style.fontSize = '15px' let textAlert = document.createElement('div') let textAlert2 = document.createElement('div') let putInText = "Введите данные!" let userAlready = "Пользователь с таким логином уже зарегистрирован!" 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(inputLogin.value, inputPassword.value)) let tokenPromise = async () => await store.dispatch(actionFullLogin(inputLogin.value, inputPassword.value)) if(inputLogin.value == '' || inputPassword.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(textAlert , textAlert2) form.append(button) main.append(form) }, orderFind(){ store.dispatch(orderFind()) }, car(){ main.innerHTML = '

    Корзина

    ' 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 += `Товар: ${good.name}

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

    ` const calculation = document.createElement('div') const buttonAdd = document.createElement('button') buttonAdd.innerHTML = '+' buttonAdd.style.width = '35px' 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 = `
    Всего: ${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.style.width = '35px' 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 = `
    Всего: ${goodPrice()} грн.
    ` } } calculation.append(buttonLess) const buttonDelete = document.createElement('button') buttonDelete.innerText = 'Удалить' buttonDelete.className = 'buttonDelete' buttonDelete.onclick = async () => { await store.dispatch(actionCartDelete({_id: _id, name: good.name, price: good.price, images: good.images})) card.style.display = 'none' cardTotal.innerHTML = `
    Всего: ${goodPrice()} грн.
    ` } card.append(calculation) card.append(buttonDelete) main.append(card) } const cardTotalDiv = document.createElement('div') const cardTotal = document.createElement('div') cardTotalDiv.style = 'position : absolute; display : flex;right : 50px; bottom: 0px' cardTotal.innerHTML = `
    Всего: ${goodPrice()} грн.` cardTotalDiv.append(cardTotal) if(localStorage.authToken!=''){ const button = document.createElement('button') button.innerHTML += 'ЗАКАЗАТЬ' button.style = ' background-color : yellowgreen; font-family: Impact; font-size : 40px' 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"; } button.className = 'btn btn-primary' cardTotalDiv.append(button) } main.append(cardTotalDiv) } } if (route in routes) routes[route]() } 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 }