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 combineReducers(reducers) {
return (state={}, action) => {
const newState = {}
// перебрать все редьюсеры
if (reducers) {
for (const [reducerName, reducer] of Object.entries(reducers)) {
const newSubState = reducer(state[reducerName], action)
if (newSubState !== state[reducerName]) {
newState[reducerName] = newSubState
}
}
// если newState не пустой, то вернуть стейт в
if (Object.keys(newState).length !== 0) {
return {...state, ...newState}
} else {
return state
}
}
}
}
const combinedReducer = combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer})
const store = createStore(combinedReducer)
store.subscribe(() => console.log(store.getState()))
function jwtDecode(token) {
try {
let decoded = JSON.parse(atob(token.split('.')[1]))
return decoded
} catch (err) {
console.log(err)
}
}
function authReducer(state, {type, token}) {
if (!state) {
if (localStorage.authToken) {
token = localStorage.authToken
type = 'AUTH_LOGIN'
} else {
return {}
}
}
if (type === 'AUTH_LOGIN') {
let payload = jwtDecode(token)
if (typeof payload === 'object') {
localStorage.authToken = token
return {
...state,
token,
payload
}
} else {
return state
}
}
if (type === 'AUTH_LOGOUT') {
delete localStorage.authToken
return {}
}
return state
}
const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
function cartReducer (state={}, {type, good={}, count=1}) {
// if (!state) {
// localStorage.cart = JSON.stringify({})
// return {}
// }
if (Object.keys(state).length === 0 && localStorage.cart) {
let currCart = JSON.parse(localStorage.cart)
if (currCart && Object.keys(currCart).length !== 0) {
state = currCart
}
}
// только если в функции задан count по умолчанию вызывать тут
// а так лучше вызвать в типах add и change
// count = +count
// if (!count) {
// return state
// }
const {_id} = good
const types = {
CART_ADD() {
count = +count
if (!count) {
return state
}
let newState = {
...state,
[_id]: {good, count: (count + (state[_id]?.count || 0)) < 0 ? 0 : count + (state[_id]?.count || 0)}
}
localStorage.cart = JSON.stringify(newState)
return newState
},
CART_CHANGE() {
count = +count
if (!count) {
return state
}
let newState = {
...state,
[_id]: {good, count: count < 0 ? 0 : count}
}
localStorage.cart = JSON.stringify(newState)
return newState
},
CART_REMOVE() {
let { [_id]: removed, ...newState } = state
localStorage.cart = JSON.stringify(newState)
return newState
},
CART_CLEAR() {
localStorage.cart = JSON.stringify({})
return {}
},
}
if (type in types) {
return types[type]()
}
return state
}
const actionCartAdd = (good, count) => ({type: 'CART_ADD', good, count})
const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
const actionCartRemove = (good) => ({type: 'CART_REMOVE', good})
const actionCartClear = () => ({type: 'CART_CLEAR'})
// store.dispatch(actionCartAdd({_id: '111111'}))
// store.dispatch(actionCartChange({_id: '111111'}, 10))
// store.dispatch(actionCartAdd({_id: '22222'}))
// store.dispatch(actionCartRemove({_id: '22222'}))
// store.dispatch(actionCartClear())
function promiseReducer(state={}, {type, status, payload, error, name}) {
if (!state) {
return {}
}
if (type === 'PROMISE') {
return {
...state,
[name]: {
status: status,
payload : payload,
error: 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 data = await promise
dispatch(actionResolved(name, data))
return data
}
catch(error){
dispatch(actionRejected(name, error))
}
}
)
const getGQL = url => (
async (query, variables={}) => {
let obj = await fetch(url, {
method: 'POST',
headers: {
"Content-Type": "application/json",
...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken} : {})
},
body: JSON.stringify({ query, variables })
})
let a = await obj.json()
if (!a.data && a.errors) {
throw new Error(JSON.stringify(a.errors))
} else {
return a.data[Object.keys(a.data)[0]]
}
}
)
const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua/'
const gql = getGQL(backURL + 'graphql');
const actionOrder = () => (
async (dispatch, getState) => {
let {cart} = getState()
//магия по созданию структуры вида
//let orderGoods = [{good: {_id}, count}, {good: {_id}, count} .......]
//из структуры вида
//{_id1: {good, count},
//_id2: {good, count}}
const orderGoods = Object.entries(cart)
.map(([_id, {good, count}]) => ({good: {_id}, count}))
let result = await dispatch(actionPromise('order', gql(`
mutation newOrder($order:OrderInput){
OrderUpsert(order:$order)
{ _id total}
}
`, {order: {orderGoods}})))
if (result?._id) {
dispatch(actionCartClear())
}
})
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) {
// console.log(token)
dispatch(actionAuthLogin(token))
}
}
)
const actionRegister = (login, password) => (
actionPromise('register', gql(`mutation reg($user:UserInput) {
UserUpsert(user:$user) {
_id
}
}
`, {user: {login, password}})
)
)
const actionFullRegister = (login, password) => (
async (dispatch) => {
let regId = await dispatch(actionRegister(login, password))
if (regId) {
// console.log(regId)
dispatch(actionFullLogin(login, password))
}
}
)
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}])}))
)
const actionGoodsByUser = (_id) => (
actionPromise('goodByUser', gql(`query oUser($query: String) {
OrderFind(query:$query){
_id orderGoods{
price count total good{
_id name categories{
name
}
images {
url
}
}
}
owner {
_id login
}
}
}`,
{query: JSON.stringify([{___owner: _id}])}))
)
const actionGoodFind = (word) => (
actionPromise('goodFind', gql(`query goodById($q: String) {
GoodFind(query: $q) {
_id name price description images {
url
}
}
}`, {q: JSON.stringify([
{
$or: [{title: `/${word}/`}, {description: `/${word}/`}, {name: `/${word}/`}] //регулярки пишутся в строках
},
{
sort: [{title: 1}] //сортируем по title алфавитно
}
])
}
))
)
store.subscribe(() => {
const {promise} = store.getState()
const {rootCats} = promise
if (rootCats?.status === 'PENDING') {
aside.innerHTML = `
Loading...
`
} else {
if (rootCats?.payload) {
aside.innerHTML = ''
const regBtn = document.createElement('a')
regBtn.href = '#/register'
regBtn.classList = 'btn btn-primary logBtn'
regBtn.innerText = 'Register'
const loginBtn = document.createElement('a')
loginBtn.href = `#/login`
loginBtn.classList = 'btn btn-primary logBtn'
loginBtn.innerText = 'Login'
const logoutBtn = document.createElement('a')
logoutBtn.innerText = 'Logout'
logoutBtn.classList = 'btn btn-primary logBtn'
aside.append(regBtn, loginBtn, logoutBtn)
logoutBtn.onclick = () => {
store.dispatch(actionAuthLogout())
}
for (const {_id, name} of rootCats?.payload) {
const link = document.createElement('a')
link.classList = 'catBtn bg-light'
link.href = `#/category/${_id}`
link.innerText = name
aside.append(link)
}
}
}
})
store.dispatch(actionRootCats())
function createForm(parent, type, callback) {
parent.innerHTML = `
`
return () => window[`btn${type}`].onclick = () => {
store.dispatch(callback(window[`login${type}`].value, window[`pass${type}`].value))
window[`pass${type}`].value = ''
}
}
const createCartPage = (parent) => {
parent.innerHTML = ''
const {cart} = store.getState()
const clearBtn = document.createElement('button')
clearBtn.classList = 'btn btn-primary cartBtn'
clearBtn.innerText = "ОЧИСТИТЬ КОРЗИНУ"
if(Object.keys(cart).length !== 0) {
parent.append(clearBtn)
}
clearBtn.onclick = () => {
store.dispatch(actionCartClear())
}
const cartPage = document.createElement('div')
cartPage.classList = 'cartPage'
parent.append(cartPage)
let cartCounter = 0
for(const item in cart) {
const {good} = cart[item]
const {count, good: {_id: id, name: name, price: price, images: [{url}]}} = cart[item]
cartCounter += count*price
const card = document.createElement('div')
card.classList = 'card cartCard'
card.innerHTML = `
${count} шт
${price} грн
Итого: ${count*price} грн
`
const inputGr = document.createElement('div')
inputGr.classList = 'inputGr'
card.lastElementChild.append(inputGr)
const minusBtn = document.createElement('button')
minusBtn.classList = 'btn btn-success'
minusBtn.innerText = '-'
inputGr.append(minusBtn)
minusBtn.onclick = () => {
store.dispatch(actionCartAdd(good, -1))
}
const changeCount = document.createElement('input')
changeCount.type = 'number'
changeCount.value = count
inputGr.append(changeCount)
changeCount.oninput = () => {
store.dispatch(actionCartChange(good, changeCount.value))
}
const plusBtn = document.createElement('button')
plusBtn.classList = 'btn btn-success'
plusBtn.innerText = '+'
inputGr.append(plusBtn)
plusBtn.onclick = () => {
store.dispatch(actionCartAdd(good))
}
const deleteGood = document.createElement('button')
deleteGood.classList = 'btn btn-success'
deleteGood.innerText = 'Удалить'
deleteGood.style.display = 'block'
card.lastElementChild.append(deleteGood)
deleteGood.onclick = () => {
store.dispatch(actionCartRemove(good))
}
cartPage.append(card)
}
const total = document.createElement('h5')
total.classList = 'totalCart'
total.innerText = `Всего к оплате: ${cartCounter} грн`
const sendOrder = document.createElement('button')
sendOrder.classList = 'btn btn-primary cartBtn'
sendOrder.innerText = "ОФОРМИТЬ ЗАКАЗ"
if(Object.keys(cart).length !== 0) {
parent.append(total)
parent.append(sendOrder)
}
const {auth} = store.getState()
if (auth.token) {
sendOrder.disabled = false
} else {
sendOrder.disabled = true
}
sendOrder.onclick = () => {
store.dispatch(actionOrder())
}
}
// location.hash - адресная строка после решетки
window.onhashchange = () => {
const [,route, _id] = location.hash.split('/')
const routes = {
category(){
store.dispatch(actionCatById(_id))
},
good(){
store.dispatch(actionGoodById(_id))
},
register(){
let goRegister = createForm(main, 'Register', actionFullRegister)
goRegister()
},
login(){
let goLogin = createForm(main, 'Login', actionFullLogin)
goLogin()
},
orders(){
store.dispatch(actionGoodsByUser(_id))
},
cart(){
createCartPage(main)
},
find(){
},
}
if (route in routes) {
routes[route]()
}
}
store.subscribe(() => {
const [,route] = location.hash.split('/')
if (route === 'cart') {
createCartPage(main)
}
})
window.onhashchange()
store.subscribe(() => {
const {promise} = store.getState()
const {catById} = promise
const [,route, _id] = location.hash.split('/')
if (catById?.status === 'PENDING') {
main.innerHTML = `
`
} else {
if (catById?.payload && route === 'category'){
main.innerHTML = ''
const catBody = document.createElement('div')
catBody.classList = 'catBody'
main.append(catBody)
const {name} = catById.payload;
catBody.innerHTML = `${name}
`
if (catById.payload.subCategories) {
const linkList = document.createElement('div')
linkList.classList = 'list-group linkList'
catBody.append(linkList)
for(const {_id, name} of catById.payload.subCategories) {
const link = document.createElement('a')
link.classList = 'list-group-item list-group-item-action list-group-item-primary linkItem'
link.href = `#/category/${_id}`
link.innerText = name
catBody.append(link)
}
}
if (catById.payload.goods) {
const cardBody = document.createElement('div')
cardBody.classList = 'cardBody'
main.append(cardBody)
for (const good of catById.payload.goods){
const {_id, name, price, images} = good
const card = document.createElement('div')
card.classList = 'card goodCard'
card.innerHTML = `
${name}
${price} грн
Подробнее
`
const btnCart = document.createElement('button')
btnCart.innerText = 'В корзину'
btnCart.classList = 'btn btn-success btnCart'
btnCart.onclick = () => {
store.dispatch(actionCartAdd(good))
}
card.lastElementChild.append(btnCart)
cardBody.append(card)
}
}
}
}
})
store.subscribe(() => {
const {promise} = store.getState()
const {goodById} = promise
const [,route, _id] = location.hash.split('/');
if (goodById?.status === 'PENDING') {
main.innerHTML = `
`
} else {
if (goodById?.payload && route === 'good') {
main.innerHTML = ''
const good = goodById.payload
const {_id, name, images, price, description} = good
const card = document.createElement('div')
card.classList = 'goodPage'
card.innerHTML = `${name}
${description}
Цена - ${price} грн
`
const btnCart = document.createElement('button')
btnCart.innerText = 'В корзину'
btnCart.classList = 'btn btn-success btnCart'
btnCart.onclick = () => {
store.dispatch(actionCartAdd(good))
}
card.append(btnCart)
main.append(card);
}
}
}
)
store.subscribe(() => {
const {auth} = store.getState()
const {payload} = auth
if (payload?.sub ) {
topContaner.innerHTML = ''
const {id, login} = payload.sub
const name = document.createElement('div')
name.innerText = `ПРИВЕТ, ${login}`
topContaner.append(name)
const myOrders = document.createElement('a')
myOrders.innerText = 'Мои заказы'
myOrders.href = `#/orders/${id}`
topContaner.append(myOrders)
} else {
topContaner.innerHTML = ''
}
})
store.subscribe(() => {
const {promise} = store.getState()
const {goodByUser} = promise
const [,route] = location.hash.split('/')
if (goodByUser?.status === 'PENDING') {
main.innerHTML = `
`
} else {
if (goodByUser?.payload && route === 'orders'){
main.innerHTML = ''
const cardBody = document.createElement('div')
cardBody.classList = 'cardBody'
main.append(cardBody)
if (goodByUser.payload) {
let totalMoney = 0
for (const order of goodByUser.payload) {
if (order.orderGoods) {
for (const {price, count, total, good} of order.orderGoods) {
if (price !== null && count !== null && total !== null && good !== null) {
totalMoney += total
const {_id, name, images} = good
const card = document.createElement('div')
card.classList = 'card goodCard'
card.innerHTML = `
${name}
Куплено: ${count} по ${price} грн.
Итого: ${total} грн
Подробнее
`
cardBody.append(card)
}
}
}
}
const totalBlock = document.createElement('h3')
totalBlock.innerText = 'Итого потрачено: ' + totalMoney + ' грн'
main.append(totalBlock)
}
}
}
})
store.subscribe(() => {
const {cart} = store.getState()
let counter = 0;
for (const key in cart) {
counter += cart[key].count
}
cartCounter.innerText = counter
})
store.subscribe(() => {
const {promise} = store.getState()
const {goodFind} = promise
const [,route] = location.hash.split('/')
if (goodFind?.status === 'PENDING') {
main.innerHTML = `
Loading...
`
} else {
if (goodFind?.payload && route === 'find') {
main.innerHTML = ''
if (goodFind?.payload.length > 0) {
const cardBody = document.createElement('div')
cardBody.classList = 'cardBody'
main.append(cardBody)
for (const good of goodFind.payload) {
const {_id, name, price, images} = good
const card = document.createElement('div')
card.classList = 'card goodCard'
card.innerHTML = `
${name}
${price} грн
Подробнее
`
const btnCart = document.createElement('button')
btnCart.innerText = 'В корзину'
btnCart.classList = 'btn btn-success btnCart'
btnCart.onclick = () => {
store.dispatch(actionCartAdd(good))
}
card.lastElementChild.append(btnCart)
cardBody.append(card)
}
} else {
main.innerHTML = 'Результаты не найдены'
}
}
}
})
findField.oninput = () => {
window.location.hash = `#/find`
store.dispatch(actionGoodFind(findField.value))
}