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