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 {
return JSON.parse(atob(token.split('.')[1]))
}
catch(e){
}
}
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
location.reload()
return {}
}
return state
}
const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
function cartReducer (state={}, {type, good={}, count=1}) {
if (Object.keys(state).length === 0 && localStorage.cart) {
let currCart = JSON.parse(localStorage.cart)
if (currCart && Object.keys(currCart).length !== 0) {
state = currCart
}
}
const {_id} = good
const types = {
CART_ADD() {
count = +count
if (!count) {
return state
}
let newState = {
...state,
[_id]: {good, count: (count + (state[_id]?.count || 0)) < 1 ? 1 : 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'})
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.node.ed.asmer.org.ua/'
const gql = getGQL(backURL + 'graphql');
const actionOrder = () => (
async (dispatch, getState) => {
let {cart} = getState()
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) {
dispatch(actionAuthLogin(token))
location.hash = '#/category'
} else {
showErrorMessage('please, enter correct login and password', main)
}
}
)
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 registerId = await dispatch(actionRegister(login, password))
if (registerId) {
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}])}))
)
store.subscribe(() => {
const {promise, auth} = store.getState()
const {rootCats} = promise
if (rootCats?.status === 'PENDING') {
aside.innerHTML = ``
} else {
if (rootCats?.payload) {
aside.innerHTML = ''
authBox.innerHTML = ''
const regBtn = document.createElement('a')
regBtn.href = '#/register'
regBtn.innerText = 'Register'
const loginBtn = document.createElement('a')
loginBtn.className = 'loginBtn'
loginBtn.href = `#/login`
loginBtn.innerText = 'Login'
const logoutBtn = document.createElement('a')
logoutBtn.innerText = 'Logout'
auth.token ? authBox.append(logoutBtn) : authBox.append(regBtn, loginBtn)
logoutBtn.onclick = () => {
store.dispatch(actionAuthLogout())
}
for (const {_id, name} of rootCats?.payload) {
const link = document.createElement('a')
link.href = `#/category/${_id}`
link.innerText = name
aside.append(link)
}
}
}
})
store.dispatch(actionRootCats())
function createForm(parent, type, callback) {
let {auth} = store.getState()
let res = `
`
parent.innerHTML = res
return () => window[`btn${type}`].onclick = () => {
store.dispatch(callback(window[`login${type}`].value, window[`pass${type}`].value))
}
}
let message = document.createElement('p')
function showErrorMessage(text, parent) {
message.innerHTML = text
parent.append(message)
}
const createCartPage = (parent) => {
parent.innerHTML = ''
const {cart} = store.getState()
const clearBtn = document.createElement('button')
clearBtn.innerText = "clear all"
if(Object.keys(cart).length !== 0) {
parent.append(clearBtn)
}
clearBtn.onclick = () => {
store.dispatch(actionCartClear())
}
const cartPage = document.createElement('div')
if(Object.keys(cart).length === 0) {
showErrorMessage('Hmm... Let`s add something into the cart!', cartPage)
}
main.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.innerHTML = `
amount:
` const inputGr = document.createElement('div') card.lastElementChild.append(inputGr) const minusBtn = document.createElement('button') minusBtn.innerText = '-' inputGr.append(minusBtn) minusBtn.onclick = () => { store.dispatch(actionCartAdd(good, -1)) } const changeCount = document.createElement('input') changeCount.type = 'number' changeCount.value = count changeCount.setAttribute('min', '1') inputGr.append(changeCount) changeCount.oninput = () => { store.dispatch(actionCartChange(good, changeCount.value)) } const plusBtn = document.createElement('button') plusBtn.innerText = '+' inputGr.append(plusBtn) plusBtn.onclick = () => { store.dispatch(actionCartAdd(good)) } const deleteGood = document.createElement('button') deleteGood.innerText = 'remove item' deleteGood.style.display = 'block' card.lastElementChild.append(deleteGood) deleteGood.onclick = () => { store.dispatch(actionCartRemove(good)) } cartPage.append(card) } const total = document.createElement('h5') total.innerText = `Total: ${cartCounter} UAH` const sendOrder = document.createElement('button') sendOrder.innerText = 'Make an order' if(Object.keys(cart).length !== 0) { parent.append(total) parent.append(sendOrder) } const {auth} = store.getState() sendOrder.disabled = !auth.token; 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(){ const registerFunc = createForm(main, 'Register', actionFullRegister) registerFunc() }, login(){ const loginFunc = createForm(main, 'Login', actionFullLogin) loginFunc() }, orders(){ store.dispatch(actionGoodsByUser(_id)) }, cart(){ createCartPage(main) } } 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 = `