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 jwtDecode(token){
try {
return JSON.parse(atob(token.split('.')[1]))
}
catch(e){
}
}
function authReducer(state={}, {type, token}){
//{
// token, payload
//}
if (type === 'AUTH_LOGIN'){
//пытаемся токен раскодировать
const payload = jwtDecode(token)
if (payload){ //и если получилось
return {
token, payload //payload - раскодированный токен;
}
}
}
if (type === 'AUTH_LOGOUT'){
return {}
}
return state;
}
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 actionGoodById = _id =>
actionPromise('goodById', gql(backendURL,`query goodByID($id: String) {
GoodFindOne(query: $id) {
_id
name
description
price
images {
url
}
owner {
nick
avatar {
url
text
}
}
categories {
name
subCategories {
name
goods {
name
price
}
}
}
}
}`, {"id": JSON.stringify([{_id}])}))
function promiseReducer(state = {}, { type, name, status, payload, error }) {
if (type === "PROMISE") {
return {
...state, //.......скопировать старый state
[name]: { status, payload, error }, //....... перекрыть в нем один name на новый объект со status, payload и error
};
}
return state;
}
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})
const actionPromise = (name, promise) =>
async dispatch => {
try {
dispatch(actionPending(name))
let payload = await promise
dispatch(actionFulfilled(name, payload))
return payload
}
catch(e){
dispatch(actionRejected(name, e))
}
}
// const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
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({auth: authReducer, promise: promiseReducer})) //не забудьте combineReducers если он у вас уже есть
if (localStorage.authToken){
store.dispatch(actionAuthLogin(localStorage.authToken))
}
//const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}))
// store.subscribe(() => console.log(store.getState()))
const gql = (url, query, variables) => fetch(url, {
method: 'POST',
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({query, variables})
}).then(res => res.json())
const url = 'http://shop-roles.node.ed.asmer.org.ua'
const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/graphql'
const actionRootCats = () =>
actionPromise('rootCats', gql(backendURL, `query {
CategoryFind(query: "[{\\"parent\\":null}]"){
_id name
}
}`))
const actionCatById = (_id) => //добавить подкатегории
actionPromise('catById', gql(backendURL, `query catById($q: String){
CategoryFindOne(query: $q){
_id name goods {
_id name price images {
url
}
}
subCategories {
name
_id
}
}
}`, {q: JSON.stringify([{_id}])}))
const actionLogin = (login, password) =>
actionPromise('login', gql(backendURL,`query log($login:String, $password:String){
login(login:$login, password:$password)
}`, {login, password}))
store.dispatch(actionRootCats())
store.dispatch(actionLogin('test456', '123123'))
//show root categories aside
store.subscribe( () => {
const rootCats = store.getState().promise.rootCats?.payload?.data.CategoryFind
if (rootCats){
aside.innerHTML = ''
for (let {_id, name} of rootCats){
const a = document.createElement('a')
a.href = `#/category/${_id}`
a.innerHTML = name
aside.append(a)
}
}
})
// store.subscribe(() => {
// const catById = store.getState().catById?.payload?.data.CategoryFindOne
// })
//cards showing
store.subscribe( () => {
let {catById} = store.getState().promise;
const [,route, _id] = location.hash.split('/')
let subCats = catById.payload?.data.CategoryFindOne.subCategories
if(catById?.status === 'PENDING') { // if pending, show loading gif
main.innerHTML = ``
} else if(catById?.payload && route === 'category'){ // if pending stopped
main.innerHTML = ''
const {name, id, goods} = catById.payload?.data.CategoryFindOne
main.innerHTML = `