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)) ) 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 }) { if (type === "AUTH_LOGIN") { const payload = jwtDecode(token) if (payload) { return { token, 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.setItem('authToken', token); } } const actionAuthLogout = () => dispatch => { dispatch({ type: "AUTH_LOGOUT" }) localStorage.removeItem('authToken') } function promiseReducer(state = {}, { type, name, status, payload, error }) { ////????? //ОДИН ПРОМИС: //состояние: PENDING/FULFILLED/REJECTED //результат //ошибка: //{status, payload, error} //{ // name1:{status, payload, error} // name2:{status, payload, error} // name3:{status, payload, error} //} if (type === 'PROMISE') { //if (name == 'login' && status == 'FULFILLED') { // store.dispatch(actionAuthLogin(payload.data.login)) // state = store.getState(); //} return { ...state, [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)) if (name === 'login') { store.dispatch(actionAuthLogin(payload.data.login)) } return payload } catch (e) { console.log(e); dispatch(actionRejected(name, e)) } } function cartReducer(state = {}, { type, count = 1, good }) { // type CART_ADD CART_REMOVE CAT_CLEAR CART_DEC // { // id1: {count: 1, good{name, price, images, id}, total: price} // } if (type === "CART_ADD") { return { ...state, [good._id]: { count: count + (state[good._id]?.count || 0), good } } } if (type === "CART_CLEAR") { return {} } if (type === "CART_REMOVE") { let newState = { ...state } delete newState[good._id] return newState } if (type === "CART_DELETE") { if (state[good._id].count > 1) { return { ...state, [good._id]: { count: -count + (state[good._id]?.count || 0), good }, }; } } return state } const cartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count }) const cartClear = () => ({ type: 'CART_CLEAR' }) const cartRemove = (good) => ({ type: 'CART_REMOVE', good }) const cartDelete = (good) => ({ type: 'CART_DELETE', good }) /* store.dispatch({type: "CART_ADD", good: {_id: "чипсы"}}) ПРОБНЫЙ СТОР!!! */ const delay = (ms) => new Promise((ok) => setTimeout(() => ok(ms), ms)) 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({ auth: authReducer, promise: promiseReducer, cart: cartReducer }) ) //не забудьте combineReducers если он у вас уже есть const enterLogin = document.getElementById('enterLogin') const outLogin = document.getElementById('outLogin') const enterLogout = document.getElementById('enterLogout') const loginContainer = document.getElementById('mainLoginContainer') const enterCart = document.getElementById('enterCart') const cartContainer = document.getElementById('cartContainer') const leaveCart = document.getElementById('closeCart') const cartProductsContainer = document.getElementById('cartProductsContainer') const enterRegister = document.getElementById('enterRegister') const registerContainer = document.getElementById('registerContainer') const hideLogin = document.getElementById('hideLogin') const outRegister = document.getElementById('outRegister') if (localStorage.authToken) { store.dispatch(actionAuthLogin(localStorage.authToken)) enterLogin.style.display = 'none'; enterCart.style.display = 'block' enterLogout.style.display = 'block'; } //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 backendURL = 'http://shop-roles.node.ed.asmer.org.ua' const graphQLBackendUrl = `${backendURL}/graphql` const actionRootCats = () => actionPromise('rootCats', gql(graphQLBackendUrl, `query { CategoryFind(query: "[{\\"parent\\":null}]"){ _id name } }`)) const actionCatById = (_id) => //добавить подкатегории actionPromise('catById', gql(graphQLBackendUrl, `query categoryById($q: String){ CategoryFindOne(query: $q){ _id name goods { _id name price images { url } } } }`, { q: JSON.stringify([{ _id }]) })) const actionGoodById = (_id) => actionPromise('goodById', gql(graphQLBackendUrl, `query goodById($q: String){ GoodFindOne(query: $q){ _id name description price } }`, { q: JSON.stringify([{ _id }]) })) const actionLogin = (login, password) => actionPromise('login', gql(graphQLBackendUrl, `query log($login: String, $password: String){ login(login:$login, password: $password) }` , { login, password })) const actionRegister = (login, password) => actionPromise( "register", gql(graphQLBackendUrl, `mutation register($login: String, $password: String) { UserUpsert(user: {login: $login, password: $password}) { _id login } }`, { login: login, password: password } ) ) store.dispatch(actionRootCats()) /* const actionEnterLogin = () */ enterLogin.onclick = () => { return loginContainer.style.display = 'block' } outLogin.onclick = () => { return loginContainer.style.display = 'none' } enterLogout.onclick = () => { store.dispatch(actionAuthLogout()) } enterRegister.onclick = () => { return hideLogin.style.display = 'none', registerContainer.style.display = 'block' } outRegister.onclick = () => { return hideLogin.style.display = 'block', registerContainer.style.display = 'none' } const updateCartInfo = () => { cartProductsContainer.innerHTML = ''; cartContainer.style.display = 'block'; const cart = store.getState().cart; const ul = document.createElement('ul') cartProductsContainer.append(ul) const clearFromCartButton = document.createElement('button') clearFromCartButton.innerHTML = "Очистить" clearFromCartButton.onclick = () => { store.dispatch(cartClear()) } cartProductsContainer.append(clearFromCartButton) const buyGoods = document.createElement('button') buyGoods.innerHTML = "Купить" buyGoods.onclick = () => { store.dispatch(cartClear()), alert('Спасибо за покупку!') } cartProductsContainer.append(buyGoods) for (const [key, value] of Object.entries(cart)) { const li = document.createElement('li') const a = document.createElement('a') const removeFromCartButton = document.createElement('button') removeFromCartButton.innerText = "-" removeFromCartButton.onclick = () => { store.dispatch(cartRemove(value.good)) } a.innerHTML = value.good.name; ul.append(li) li.append(a) if (value.good.images) { for (let i = 0; i < value.good.images.length; i++) { const imgElement = document.createElement('img') imgElement.src = `${backendURL}/${value.good.images[i].url}`; imgElement.style.height = '100px'; imgElement.style.width = '200px'; li.append(imgElement) } } ul.append(removeFromCartButton) if (value.count > 1) { const deleteFromCartButton = document.createElement('button') deleteFromCartButton.innerHTML = "-1" deleteFromCartButton.onclick = () => { store.dispatch(cartDelete(value.good)) } ul.append(deleteFromCartButton) } const allCountElement = document.createElement('p'); allCountElement.innerHTML = `Total: ${value.count}`; ul.append(allCountElement) } }; enterCart.onclick = () => { updateCartInfo(); } leaveCart.onclick = () => { cartProductsContainer.innerHTML = ''; cartContainer.style.display = 'none'; } const loginBtn = document.getElementById('loginBtn') loginBtn.onclick = handleLogin function handleLogin() { let userName = document.getElementById('inputUserName') let userPassword = document.getElementById('inputPassword') store.dispatch(actionLogin(userName.value, userPassword.value)); outLogin.click(); } const registerBtn = document.getElementById('registerBtn'); registerBtn.onclick = handleRegister function handleRegister() { let userName = document.getElementById('inputUserNameRegister') let userPassword = document.getElementById('inputPasswordRegister') store.dispatch(actionRegister(userName.value, userPassword.value)); return hideLogin.style.display = 'block', registerContainer.style.display = 'none' outLogin.click(); } /* store.dispatch(actionLogin('levshin95', '123123')) */ store.subscribe(() => { if (cartContainer.style.display === 'block' && store.getState().cart) { updateCartInfo(); } }) store.subscribe(() => { const rootCats = store.getState().promise.rootCats?.payload?.data.CategoryFind if (!rootCats) { aside.innerHTML = '' } else { aside.innerHTML = '' const ul = document.createElement('ul') aside.append(ul) for (let { _id, name } of rootCats) { const li = document.createElement('li') const a = document.createElement('a') a.href = "#/category/" + _id a.innerHTML = name ul.append(li) li.append(a) } } }) const displayCategory = function () { const catById = store.getState().promise.catById?.payload?.data.CategoryFindOne const [, route] = location.hash.split('/') if (catById && route === 'category') { const { name, goods, _id } = catById categories.innerHTML = `

${name}

` const ul = document.createElement('ul') categories.append(ul) for (let good of goods) { const li = document.createElement('li') const a = document.createElement('a') a.href = "#/good/" + good._id a.innerHTML = good.name ul.append(li) li.append(a) if (good.images) { for (let i = 0; i < good.images.length; i++) { const imgElement = document.createElement('img') imgElement.src = `${backendURL}/${good.images[i].url}`; imgElement.style.height = '100px'; imgElement.style.width = '200px'; li.append(imgElement) } } const authToken = store.getState().auth?.token; if (authToken) { const addToCartButton = document.createElement('button') addToCartButton.innerText = "+" addToCartButton.onclick = () => { store.dispatch(cartAdd(good, 1)) } ul.append(addToCartButton) } } } } store.subscribe(() => { const authToken = store.getState().auth?.token; if (!authToken) { enterLogout.style.display = 'none'; enterCart.style.display = 'none'; enterLogin.style.display = 'block'; displayCategory(); } else { enterLogin.style.display = 'none'; enterCart.style.display = 'block'; enterLogout.style.display = 'block'; displayCategory(); /* alert(`Hello, ${store.getState().auth?.payload?.sub?.login}`); */ } }) store.subscribe(() => { displayCategory(); }) store.subscribe(() => { const goodById = store.getState().promise.goodById?.payload?.data.GoodFindOne const [, route] = location.hash.split('/') if (goodById && route === 'good') { const { name, description, _id, price, images } = goodById categories.innerHTML = `

${name}

` const strongPrice = document.createElement('b') const div = document.createElement('div') categories.appendChild(div) categories.appendChild(strongPrice) categories.appendChild(strongPrice) div.innerText = description strongPrice.innerHTML = price } }) store.subscribe(() => { }) /* store.subscribe(() => { const goodById = store.getState().promise.goodById?.payload?.data.CategoryFindOne const [,route] = location.hash.split('/') if (catById && route === 'good') { const {name, goods, _id} = catById categories.innerHTML = `

${name}

` // нарисовать картинки, описание, цену и т.д. } }) */ window.onhashchange = () => { const [, route, _id] = location.hash.split('/') console.log(route, _id) const routes = { category() { store.dispatch(actionCatById(_id)) }, good() { store.dispatch(actionGoodById(_id)) }, login() { console.log('Тут надо нарисовать форму логина. по нажатию кнопки в ней - задиспатчить actionFullLogin') }, register() { console.log('Тут надо нарисовать форму логина/регистрации. по нажатию кнопки в ней - задиспатчить actionFullRegister') } } if (route in routes) { routes[route]() } } window.onhashchange()