// !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
}