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 в объект
}
}
function promiseReducer(state = {}, { type, name, status, payload, error }) {
if (type === 'PROMISE') {
return {
...state,
[name]: { status, payload, error }
}
}
return state
}
const actionPending = name => ({ type: 'PROMISE', status: 'PENDING', name })
const actionResolved = (name, payload) => ({ type: 'PROMISE', status: 'RESOLVED', name, payload })
const actionRejected = (name, error) => ({ type: 'PROMISE', status: 'REJECTED', name, error })
const actionPromise = (name, promise) =>
async dispatch => {
dispatch(actionPending(name))
try {
let payload = await promise
dispatch(actionResolved(name, payload))
return payload
}
catch (error) {
dispatch(actionRejected(name, error))
}
}
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.errors && !data.data)
throw new Error(JSON.stringify(data.errors))
return data.data[Object.keys(data.data)[0]]
})
const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
const gql = getGQL(`${backURL}/graphql`)
const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
function jwtDecode(token) {
try {
let decoded = token.split('.')
decoded = decoded[1]
decoded = atob(decoded)
decoded = JSON.parse(decoded)
return decoded
} catch (e) {
return;
}
}
function authReducer(state, { type, token }) {
if (!state) {
if (!localStorage.authToken) {
console.log('NO-TOKEN')
return {}
} else {
type = 'AUTH_LOGIN'
token = localStorage.authToken
}
}
if (type === 'AUTH_LOGIN') {
console.log('AUTH-LOGIN')
let decoded = jwtDecode(token)
if (decoded) {
localStorage.authToken = token
return { token, payload: decoded }
}
}
if (type === 'AUTH_LOGOUT') {
console.log('AUTH-LOGOUT')
localStorage.removeItem('authToken')
return {}
}
return state
}
function combineReducers(reducers) {
return (state = {}, action) => {
const newState = {}
for (const [reducerName, reducer] of Object.entries(reducers)) {
let newSubState = reducer(state[reducerName], action)
if (newSubState !== state[reducerName]) {
newState[reducerName] = newSubState
}
}
if (Object.keys(newState).length !== 0) {
return { ...state, ...newState }
}
return state
}
}
const combinedReducer = combineReducers({ promise: promiseReducer, auth: authReducer, cart: cartReducer })
const store = createStore(combinedReducer)
const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token })
const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' })
console.log(store.getState()) //стартовое состояние может быть с токеном
store.subscribe(() => console.log(store.getState()))
const actionLogin = (login, password) =>
actionPromise('login', gql(`
query log($login:String, $password:String) {
login(login: $login, password: $password)
}`, { login, password }))
const actionFullLogin = (login, password) =>
async dispatch => {
let token = await dispatch(actionLogin(login, password))
if (token) {
dispatch(actionAuthLogin(token))
}
}
const actionGetOrders = () =>
actionPromise('orders', gql(`
query ord {
OrderFind(query:"[{}]"){
_id
orderGoods {
price
count
total
good{
name
_id
images {
url
}
}
}
}
}
`))
const actionCreateOrder = (count, id) =>
actionPromise('createOrder', gql(`
mutation createOrder($count:Int!, $id:ID) {
OrderUpsert( order:{ orderGoods: [ {count: $count, good:{_id: $id}} ] } ) {
_id
total
orderGoods {
good { _id name }
}
}
}
`, {count, id}))
const actionRegister = (login, password) =>
actionPromise('registration', gql(`
mutation register($login:String, $password:String) {
UserUpsert(
user: {
login: $login,
password: $password,
}){
login _id
}
}
`, { login, password }))
let logBtn = document.getElementById('logBtn')
function cartReducer(state = {}, { type, good = {}, count = 1 }) {
let { _id } = good
console.log('_id from state',_id)
const types = {
CART_ADD() {
return {
...state,
[_id]: { good, count: (state[_id]?.count || 0) + count }
}
},
CART_REMOVE() {
let newState = {}
for(let key in state) {
if(key !== _id) {
newState[key] = state[key]
}
}
return newState
},
CART_CHANGE() {
return {
...state,
[_id]: { good, count }
}
},
CART_CLEAR() {
return {}
},
}
if (type in types)
return types[type]()
return state
}
const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count })
const actionCartDel = (good) => ({ type: 'CART_REMOVE', good})
const actionCartClear = () => ({type: 'CART_CLEAR'})
const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
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
subCategories {
_id name
}
goods {
_id name price images {
url
}
}
}
}`, { q: JSON.stringify([{ _id }]) }))
store.dispatch(actionRootCats())
const actionGoodById = (_id) =>
actionPromise('goodById', gql(`
query goodById ($good:String) {
GoodFindOne(query: $good) {
name
description
price
categories {
name
}
images {
url
}
}
}`, { good: JSON.stringify([{ _id }]) }))
store.subscribe(() => {
const { rootCats } = store.getState().promise
if (rootCats?.payload) {
aside.innerHTML = ''
for (const { _id, name } of rootCats?.payload) {
const link = document.createElement('a')
link.href = `#/category/${_id}`
link.innerText = name
aside.append(link)
}
}
})
window.onhashchange = () => {
const [, route, _id] = location.hash.split('/')
const routes = {
category() {
store.dispatch(actionCatById(_id))
},
good() {
store.dispatch(actionGoodById(_id))
},
registration() {
let navBar = document.getElementById('navBar')
navBar.style.visibility = 'hidden'
let pass, log
aside.innerHTML = ''
main.innerHTML = `
⇐ назад
РЕГИСТРАЦИЯ
`
let returnBtn = document.getElementById('returnBtnReg')
let logInp = document.getElementById('logInp')
let passInp = document.getElementById('passInp')
let registerBtn = document.getElementById('registerBtn')
let errBox = document.getElementById('errBox')
passInp.oninput = (e) => { pass = e.target.value }
logInp.oninput = (e) => { log = e.target.value }
returnBtn.onclick = () => {
navBar.style.visibility = 'visible'
history.back()
}
registerBtn.onclick = () => {
console.log('login clicked')
if (log && pass) {
(async () => {
await store.dispatch(actionRegister(log, pass))
aside.innerHTML = ``
if (store.getState().promise.registration.payload) {
errBox.innerText = ''
main.innerHTML = `
Пользователь под логином '${store.getState().promise.registration.payload.login} успешно зарегистрирован!'
⇐ Вернуться к просмотру
  или  
Авторизоваться
`
let retBtn = document.getElementById('returnBtn')
retBtn.onclick = () => {
navBar.style.visibility = 'visible'
location.href = "#/category"
}
} else {
errBox.innerText = 'Пользователь с таким логином уже существует'
}
})();
}
}
},
login() {
let navBar = document.getElementById('navBar')
navBar.style.visibility = 'hidden'
let pass, log
console.log('login')
aside.innerHTML = ''
main.innerHTML = `
⇐ назад
ВОЙТИ
`
let returnBtn = document.getElementById('returnBtnLogin')
let logInp = document.getElementById('logInp')
let passInp = document.getElementById('passInp')
let submitBtn = document.getElementById('submitBtn')
let errBox = document.getElementById('errBox')
passInp.oninput = (e) => { pass = e.target.value }
logInp.oninput = (e) => { log = e.target.value }
returnBtn.onclick = () => {
navBar.style.visibility = 'visible'
history.back()
}
submitBtn.onclick = () => {
if (log && pass) {
(async () => {
await store.dispatch(actionFullLogin(log, pass))
aside.innerHTML = ``
if (store.getState().auth.payload) {
errBox.innerText = ''
store.dispatch(actionGetOrders())
} else {
errBox.innerText = 'Неправильный логин или пароль'
}
})();
}
}
},
cart() {
aside.innerHTML = ''
main.innerHTML = `
⇐ назад
Корзина
Общая цена заказов:
Очистить корзину
Заказать все
`
let cartContent = document.getElementById('cartContent')
let orderAllBtn = document.getElementById('orderAll')
if(localStorage.authToken) {
orderAllBtn.disabled = false
orderAllBtn.style.backgroundColor = "mediumseagreen"
orderAllBtn.style.color = 'white'
} else {
orderAllBtn.disabled = true
orderAllBtn.style.backgroundColor = "grey"
orderAllBtn.style.color = 'whitesmoke'
}
orderAllBtn.onclick = () => {
console.log('orderAll clicked');
(async () => {
let cart = store.getState().cart
await store.dispatch(actionCartClear())
for(let item in cart) {
await store.dispatch(actionCreateOrder(cart[item].count, cart[item].good._id))
drawCart()
}
await store.dispatch(actionGetOrders())
aside.innerHTML = ''
})()
}
let clearCartBtn = document.getElementById('clearCartBtn')
clearCartBtn.onclick = () => {
(async () => {
await store.dispatch(actionCartClear())
drawCart()
})()
}
function drawCart() {
let fullPrice = 0
let cart = store.getState().cart
cartContent.innerHTML = ``
if(Object.keys(cart).length > 0) {
for (let item in cart) {
let orderPrice = cart[item].good.price * cart[item].count
fullPrice += orderPrice
let delOrderBtn = document.createElement('button')
delOrderBtn.style.backgroundColor = "firebrick"
delOrderBtn.style.color = "white"
delOrderBtn.style.fontSize = "smaller"
delOrderBtn.style.float = "right"
delOrderBtn.innerText = "Удалить заказ [x]"
delOrderBtn.onclick = () => {
(async () => {
await store.dispatch(actionCartDel(cart[item].good))
drawCart()
})()
}
let card = document.createElement('div')
card.append(delOrderBtn)
card.insertAdjacentHTML('beforeend', `
${cart[item].good.name}
Кол-во: - ${cart[item].count} +
Стоимость заказа: ${orderPrice}
${localStorage.authToken? 'Заказать' : 'Только авторизованные пользователи могут совершать заказы'}
Цена за ед. товара: ${cart[item].good.price}
На страницу товара
`)
card.style.backgroundColor = "whitesmoke"
card.style.border = "1px solid black"
card.style.margin = "10px"
card.style.padding = "10px"
card.querySelector('.decBtn').disabled = (cart[item].count <= 1)
card.querySelector('.decBtn').onclick = () => {
(async() => {
await store.dispatch(actionCartChange(cart[item].good, cart[item].count-1))
drawCart()
})()
}
card.querySelector('.addBtn').onclick = () => {
(async() => {
await store.dispatch(actionCartChange(cart[item].good, cart[item].count+1))
drawCart()
})()
}
card.querySelector('.makeOrderBtn').onclick = () => {
(async() => {
await store.dispatch(actionCreateOrder(cart[item].count, cart[item].good._id))
await store.dispatch(actionCartDel(cart[item].good))
await store.dispatch(actionGetOrders())
drawCart()
})()
}
cartContent.append(card)
}
}
aside.innerHTML = ``
let fullPriceSpan = document.getElementById('fullPrice')
fullPriceSpan.innerText = `${fullPrice}`
}
drawCart()
},
orders() {
aside.innerHTML = ''
main.innerHTML = `
⇐ назад
Список заказов ${store.getState().auth.payload.sub.login}
`
let container = document.createElement('div')
main.append(container)
if (store.getState().promise.orders) {
const payload = store.getState().promise.orders.payload
if (payload.length > 0) {
for (let { orderGoods } of payload) {
for (let item of orderGoods) {
try {
let card = document.createElement('div')
card.insertAdjacentHTML('beforeend', `
${item.good.name}
Кол-во: ${item.count}
Стоимость заказа: ${item.total}
Цена за ед. товара: ${item.price}
На страницу товара
`)
card.style.backgroundColor = "whitesmoke"
card.style.border = "1px solid black"
card.style.margin = "10px"
card.style.padding = "10px"
container.prepend(card)
} catch (e) {
console.log('nulls are ignored')
}
}
}
}
}
}
}
if (route in routes)
routes[route]()
}
window.onhashchange()
//drawing goood list of cathegory
store.subscribe(() => {
const { catById } = store.getState().promise
const [, route, _id] = location.hash.split('/')
if (catById?.payload && route === 'category') {
const { name, subCategories } = catById.payload
main.innerHTML = `${name} `
if (subCategories) {
console.log('here', subCategories)
let subCats = document.createElement('div')
for (let item of subCategories) {
let link = document.createElement('a')
link.innerHTML = `${item.name} ⏎`
link.href = `#/category/${item._id}`
link.style.margin = '5px'
link.style.display = 'inline-block'
link.style.padding = '5px'
link.style.backgroundColor = 'black'
link.style.color = 'white'
subCats.append(link)
}
main.append(subCats)
}
main.style.padding = "10px"
if (catById.payload.goods) {
for (const good of catById.payload.goods) {
const card = document.createElement('div')
card.innerHTML = `
${good.name}
Price: ${good.price}
На страницу товара
`
card.style.backgroundColor = "whitesmoke"
card.style.border = "1px solid black"
card.style.margin = "10px"
card.style.padding = "10px"
main.append(card)
let cartButton = document.createElement('button')
cartButton.innerText = 'Добавить в корзину'
cartButton.style.fontSize = 'smaller'
cartButton.style.marginTop = '10px'
cartButton.style.backgroundColor = 'gold'
card.append(cartButton)
cartButton.onclick = () => {
store.dispatch(actionCartAdd(good))
}
}
}
}
})
//item page
store.subscribe(() => {
const { goodById } = store.getState().promise
const [, route, _id] = location.hash.split('/')
if (goodById?.payload && route === 'good') {
const { name, categories, images, price, description } = goodById.payload
main.innerHTML = `
⇐ назад
${name}
${categories[0].name}
Price: ${price}
${description}
`
main.style.padding = "10px"
}
})
//cntrol over login
store.subscribe(() => {
const { payload } = store.getState().auth
const [, route, _id] = location.hash.split('/')
let logBtn = document.getElementById('logBtn')
let logLink = document.getElementById('logLink')
let regLink = document.createElement('a')
let nameField = document.getElementById('nameField')
let navBar = document.getElementById('navBar')
if (!payload) {
nameField.innerText = ''
logBtn.style.backgroundColor = 'mediumaquamarine'
logLink.innerText = 'Войти'
logBtn.onclick = () => {/*killing onclick login action*/ }
if (!document.getElementById('regLink')) {
regLink.className = 'linkDeco'
regLink.innerText = 'Регистрация'
regLink.href = `#/registration`
regLink.style.backgroundColor = 'mediumvioletred'
regLink.id = 'regLink'
logBtn.after(regLink)
}
try {
let orderBtn = document.getElementById('orderBtn')
navBar.removeChild(orderBtn)
} catch (e) { }
} else if (payload && route === 'login') {
aside.innerHTML = ``
main.innerHTML = `
Вы успешно вошли под логином пользователя '${payload.sub.login}'
⇐ Вернуться к просмотру
`
let retBtn = document.getElementById('returnBtn')
retBtn.onclick = () => {
navBar.style.visibility = 'visible'
location.href = "#/category/5dc49f4d5df9d670df48cc64"
}
} else {
nameField.innerText = payload.sub.login
logBtn.style.backgroundColor = 'firebrick'
logBtn.innerHTML = 'Выйти'
try {
let _regLink = document.getElementById('regLink')
_regLink.parentElement.removeChild(_regLink)
} catch (e) { }
if (!document.getElementById('orderBtn')) {
let orderBtn = document.createElement('a')
orderBtn.href = '#/orders'
orderBtn.id = 'orderBtn'
orderBtn.innerText = 'список заказов'
orderBtn.className = 'linkDeco'
navBar.append(orderBtn)
}
logBtn.onclick = () => {
nameField.innerText = ''
logBtn.style.backgroundColor = 'mediumaquamarine'
logBtn.innerHTML = `Войти `
store.dispatch(actionAuthLogout())
store.dispatch(actionGetOrders())
}
}
})
//count cart items
store.subscribe(() => {
const { cart } = store.getState()
let items = 0
for (let item in cart) {
items += cart[item].count
}
let itemCount = document.getElementById('itemCount')
itemCount.innerText = items
})
window.onload = () => {
location.href = "#/category/5dc49f4d5df9d670df48cc64"
store.dispatch(actionGetOrders())
}