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 promiseReducer(state = {}, {type, name, status, payload, error}) { if (type === 'PROMISE') { return { ...state, [name]: {status, payload, error} }; } return state; } function jwtDecode(token) { try { let newToken = token.split('.')[1]; return JSON.parse(atob(newToken)); } catch (e) { console.error(e); } //раскодировать токен: //выкусить середочку //atob //JSON.parse //на любом этапе могут быть исключения } function getTokenFromLS() { const token = localStorage.getItem('auth'); return !!token && token !== 'undefined' ? {token} : {}; } function authReducer(state = getTokenFromLS(), {type, token}) { if (!state) { if (localStorage.getItem('auth')) { type = 'AUTH_LOGIN'; token = localStorage.getItem('auth'); } else { return {}; } //проверить localStorage.authToken на наличие //если есть - сделать так, что бы следующий if сработал //если нет - вернуть {} } if (type === 'AUTH_LOGIN') { // token = localStorage.getItem('authToken'); let decodeToken = jwtDecode(token); //взять токен из action //попытаться его jwtDecode //если удалось, то: //сохранить токен в localStorage //вернуть объект вида {токен, payload: раскодированный токен} return { token, payload: decodeToken, }; } if (type === 'AUTH_LOGOUT') { localStorage.removeItem('auth'); return {}; //почистить localStorage //вернуть пустой объект } console.log(state); return state; } function combineReducers(reducers) { return (state = {}, action) => { const newState = {}; for (const [reducerName, reducer] of Object.entries(reducers)) { const newSubState = reducer(state[reducerName], action); if (newSubState !== state[reducerName]) { newState[reducerName] = newSubState; } } if (Object.keys(newState).length !== 0) { return {...state, ...newState}; } else { return state; } //перебрать все редьюсеры //запустить каждый их них //передать при этом в него ЕГО ВЕТВЬ общего state, и action как есть //получить newSubState //если newSubState отличается от входящего, то записать newSubState в newState //после цикла, если newState не пуст, то вернуть {...state, ...newState} //иначе вернуть state }; } const combinedReducer = combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}); const store = createStore(combinedReducer); const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token}); const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'}); function getCartFromLS() { const cart = JSON.parse(localStorage.getItem('cart')); return !!cart ? cart : {}; }//?? function cartReducer(state = getCartFromLS(), {type, good = {}, count = 1}) { const {_id} = good; // const {count} = good; // { // _id1: {good, count} // _id2: {good, count} // } const types = { CART_ADD() { //как CHANGE, только если ключ раньше был, то достать из count и добавить //к count из action. Если не было, достать 0 и добавить к count из action return { ...state, //по аналогии с promiseReducer дописать [_id]: {good, count: count + (state[_id]?.count || 0)} }; }, CART_REMOVE() { //смочь скопировать объект и выкинуть ключ. как вариант через //деструктуризацию return Object.fromEntries(Object.entries(state).filter(([key, value]) => { return key !== _id; })); }, CART_CHANGE() { return { ...state, //по аналогии с promiseReducer дописать [_id]: {good, count} }; }, CART_CLEAR() { return {}; }, }; if (type in types) { return types[type](); } return state; } //понаписывать action //прикрутить к товару кнопку которая делает store.dispatch(actionCartAdd(good)) // store.dispatch({type: 'CART_CHANGE', good: {_id: 'пиво', name: 'пиво'}, count: 10}); // console.log(store.dispatch({type: 'CART_CHANGE', good: {_id: 'пиво', name: 'пиво'}, count: 10})); // store.dispatch({type: 'CART_CHANGE', good: {_id: 'вода', name: 'вода'}, count: 10}); // store.dispatch({type: 'CART_ADD', good: {_id: 'вода', name: 'вода'}, count: 6}); // store.dispatch({type: 'CART_ADD', good: {_id: 'вода', name: 'вода'}, count: 12}); // store.dispatch({type: 'CART_CHANGE', good: {_id: 'сок', name: 'сок'}, count: 10}); // store.dispatch({type: 'CART_REMOVE', good: {_id: 'вода', name: 'вода'}, count: 9}); //ПЕРЕДЕЛАТЬ ОТОБРАЖЕНИЕ с поправкой на то, что теперь промисы не в корне state а в state.promise const actionLogin = (login, password) => actionPromise('login', gql(`query find($login: String, $password: String){ login(login:$login, password: $password) }`, {login: login, password: password})); const actionFullLogin = (login, password) => async dispatch => { const token = await dispatch(actionLogin(login, password)); if (token) { await dispatch(actionAuthLogin(token)); await dispatch(actionMyOrders()); } }; const actionRegister = (login, password) => actionPromise('register', gql(`mutation reg($login: String, $password: String){ UserUpsert(user:{login:$login, password: $password, nick:$login}){ _id login } }`, {login: login, password: password}));//actionPromise const actionFullRegister = (login, password) => //actionRegister + actionFullLogin async dispatch => { try { await dispatch(actionRegister(login, password)); } catch (e) { return console.log(e); } await dispatch(actionFullLogin(login, password)); }; // + интерфейс к этому - форму логина, регистрации, может повесить это на #/login #/register // + #/orders показывает ваши бывшие заказы: // сделать actionMyOrders const actionCartAdd = (good, count = 1) => ({type: 'CART_ADD', good, count}); const actionCartChange = (good, count = 1) => ({type: 'CART_CHANGE', good, count}); const actionCartDelete = (good, count = 0) => ({type: 'CART_REMOVE', good, count}); const actionCartClear = () => ({type: 'CART_CLEAR'}); store.subscribe(() => console.log(store.getState())); //проверить: //поделать store.dispatch с разными action. Скопипастить токен //проверить перезагрузку страницы. 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)); // 1. {delay1000: {status: 'PENDING'}} try { let payload = await promise; dispatch(actionResolved(name, payload)); return payload; } catch (error) { dispatch(actionRejected(name, error)); } }; const getGQL = url => (query, variables = {}) => fetch(url, { //метод method: 'POST', headers: { //заголовок content-type 'Content-Type': 'application/json', ...(localStorage.getItem('auth') ? {'Authorization': 'Bearer ' + localStorage.getItem('auth')} : {}) }, //body с ключами query и variables body: JSON.stringify({query, variables}) }) .then(res => res.json()) .then(data => { if (data.errors && !data.data) throw new Error(JSON.stringify(data.errors)); return data.data[Object.keys(data.data)[0]]; }); const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'; const gql = getGQL(`${backURL}/graphql`); 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 description images { url _id good{ _id name description images{ _id url } } } } subCategories{ _id name image{ url } } } }`, {q: JSON.stringify([{_id}])})); const actionGoodById = (_id) => actionPromise('goodById', gql(` query goodById ($good:String) { GoodFindOne(query: $good) { name description price categories { name } images { url } } }`, {good: JSON.stringify([{_id}])})); store.dispatch(actionRootCats()); const actionOrder = () => async (dispatch, getState) => { let {cart} = getState(); const orderGoods = Object.entries(cart) .map(([_id, {...key}]) => ({good: {_id}, count: key.count})); let result = await dispatch(actionPromise('order', gql(` mutation newOrder($order:OrderInput){ OrderUpsert(order:$order) { _id total orderGoods{ price count good{ name createdAt price images{ url } } } } } `, {order: {orderGoods}}))); if (result?._id) { dispatch(actionCartClear()); } }; actionMyOrders = () => actionPromise('myOrders', gql( `query o{ OrderFind(query:"[{}]"){ _id total orderGoods{ price count total good{ createdAt name price images{ url text } } } } }`)); store.subscribe(() => { const {rootCats} = store.getState().promise; if (rootCats?.payload) { aside.innerHTML = ''; for (const {_id, name} of rootCats?.payload) { const link = document.createElement('a'); link.href = `#/category/${_id}`; link.innerText = name; aside.append(link); } } }); window.onhashchange = () => { const [, route, _id] = location.hash.split('/'); const routes = { category() { store.dispatch(actionCatById(_id)); console.log('КАТЕГОРИИСТРАНИЦА'); }, good() { //задиспатчить actionGoodById store.dispatch(actionGoodById(_id)); console.log('ТОВАРОСТРАНИЦА'); }, login() { console.log('LOGIN'); main.innerHTML = ''; const token = localStorage.getItem('auth'); if (!token || token === 'undefined') { const h2Greeting = document.createElement('h2'); h2Greeting.textContent = 'Вход в личный кабинет'; const loginInput = document.createElement('input'); loginInput.type = 'login'; loginInput.className = 'loginInput'; loginInput.placeholder = 'Логин'; const pswInput = document.createElement('input'); pswInput.type = 'password'; pswInput.className = 'pswInput'; pswInput.placeholder = 'Пароль'; const buttonSend = document.createElement('button'); buttonSend.className = 'buttonSend'; buttonSend.textContent = 'Войти'; buttonSend.onclick = () => { if (loginInput.value !== '' && pswInput.value !== '') { loginInput.style.borderColor = '#ccc'; pswInput.style.borderColor = '#ccc'; store.dispatch(actionFullLogin(loginInput.value, pswInput.value)); console.log('нажала на логин'); location.href = `#/dashboard/${_id}`; } else { loginInput.style.borderColor = 'red'; pswInput.style.borderColor = 'red'; loginInput.placeholder = 'Введите логин!'; pswInput.placeholder = 'Введите пароль!'; } }; main.appendChild(h2Greeting); main.appendChild(loginInput); main.appendChild(pswInput); main.appendChild(buttonSend); const divQuestion = document.createElement('div'); divQuestion.textContent = 'Вы еще не зарегистрированы?'; divQuestion.className = 'divQuestion'; const a = document.createElement('a'); a.className = 'link'; a.href = `#/register/${_id}`; a.textContent = 'Регистрация'; main.appendChild(divQuestion); main.appendChild(a); console.log('Задиспатчила логин и пароль по клику'); } else { location.href = `#/dashboard/${_id}`; //?? console.log('перехожу в доску заказов, птмш уже авторизована'); } }, register() { console.log('я в форме регистрации'); main.innerHTML = ''; const h2Greeting = document.createElement('h2'); h2Greeting.textContent = 'Регистрация нового пользователя'; const loginInputForName = document.createElement('input'); loginInputForName.type = 'text'; loginInputForName.className = 'loginInput'; loginInputForName.placeholder = 'Ваше имя'; const loginInputForSurname = document.createElement('input'); loginInputForSurname.type = 'text'; loginInputForSurname.className = 'loginInput'; loginInputForSurname.placeholder = 'Ваша фамилия'; const loginInput = document.createElement('input'); loginInput.type = 'text'; loginInput.className = 'loginInput'; loginInput.placeholder = 'Логин*'; const pswInput = document.createElement('input'); pswInput.type = 'password'; pswInput.className = 'pswInput'; pswInput.placeholder = 'Пароль*'; const buttonSend = document.createElement('button'); buttonSend.className = 'buttonSend'; buttonSend.textContent = 'Зарегистрироваться'; main.appendChild(h2Greeting); main.appendChild(loginInputForName); main.appendChild(loginInputForSurname); main.appendChild(loginInput); main.appendChild(pswInput); main.appendChild(buttonSend); buttonSend.onclick = function register(e) { store.dispatch(actionFullRegister(loginInput.value, pswInput.value)); const a = document.createElement('a'); a.href = `#/login/${_id}`; a.textContent = 'Войти в личный кабинет'; main.appendChild(a); }; console.log('передаю данные на регистрацию'); }, cart() { if (Object.keys(store.getState().cart).length !== 0) { main.innerHTML = ''; for (const key in store.getState().cart) { const {good} = store.getState().cart[key]; let {count} = store.getState().cart[key]; let {name, price, images} = good; const headerName = document.createElement('h2'); headerName.innerHTML = name; main.appendChild(headerName); const img = document.createElement('img'); img.src = `${backURL}/${images[0].url}`; main.appendChild(img); let currentPrice = price * count; const divPrice = document.createElement('div'); divPrice.innerHTML = `${currentPrice}грн`; main.appendChild(divPrice); const wrapperForCounter = document.createElement('div'); wrapperForCounter.className = 'wrapperForCounter'; const inputChangeNumber = document.createElement('input'); inputChangeNumber.className = 'inputChangeNumber'; inputChangeNumber.type = 'number'; inputChangeNumber.value = count; inputChangeNumber.onclick = () => { const inputValue = inputChangeNumber.value; divPrice.innerHTML = `${price * +inputValue}грн`; store.dispatch(actionCartChange(good, +inputValue)); }; wrapperForCounter.appendChild(inputChangeNumber); const buttonDeleteGood = document.createElement('input'); buttonDeleteGood.type = 'button'; buttonDeleteGood.className = 'buttonDeleteGood'; buttonDeleteGood.value = 'х'; buttonDeleteGood.onclick = () => { store.dispatch(actionCartDelete(good, inputChangeNumber.value)); window.location.reload() }; wrapperForCounter.appendChild(buttonDeleteGood); main.appendChild(wrapperForCounter); } const buttonSend = document.createElement('button'); buttonSend.className = 'buttonSend'; buttonSend.textContent = 'Оформить заказ'; main.appendChild(buttonSend); buttonSend.onclick = () => { store.dispatch(actionOrder(_id)); location.href = `#/dashboard/${_id}`; }; console.log('СТРАНИЦА КОРЗИНЫ'); } else { main.innerHTML = 'Ваша корзина пуста :('; } }, dashboard() { store.dispatch(actionMyOrders()); main.innerHTML = ''; const wrapperForPrivate = document.createElement('div'); wrapperForPrivate.className = 'wrapperForPrivate'; const divNameofPage = document.createElement('h2'); divNameofPage.className = 'divNameofPage'; divNameofPage.textContent = 'Личный кабинет'; wrapperForPrivate.appendChild(divNameofPage); const buttonLogOff = document.createElement('button'); buttonLogOff.className = 'buttonLogOff'; buttonLogOff.textContent = 'Выход'; wrapperForPrivate.appendChild(buttonLogOff); buttonLogOff.onclick = () => { store.dispatch(actionAuthLogout()); main.innerHTML = 'Вы вышли с личного кабинета! До новых встреч :)'; //?? }; main.appendChild(wrapperForPrivate); const headerOrders = document.createElement('div'); headerOrders.style.fontSize = '20px'; headerOrders.textContent = 'История заказов'; main.appendChild(headerOrders); console.log('СТОРЕ ДИСПАТЧ ПРОЧИТАТЬ БЫВШИЕ ЗАКАЗЫ'); } }; if (route in routes) { routes[route](); } }; window.onhashchange(); store.subscribe(() => { const [, route, _id] = location.hash.split('/'); console.log(route); if (route === 'dashboard') { console.log('ДОСКА ЗАКАЗОВ'); const auth = store.getState().auth; if (Object.keys(auth).length !== 0) { localStorage.setItem('auth', auth.token); const myOrders = store.getState().promise?.myOrders; if (myOrders?.payload) { myOrders?.payload.filter(order => { const {total, orderGoods} = order; if (total !== null) { const divDashboardOrders = document.createElement('h2'); divDashboardOrders.className = 'divDashboardOrders'; main.appendChild(divDashboardOrders); const divDescriptionOrder = document.createElement('div'); divDescriptionOrder.className = 'divDescriptionOrder'; const img = document.createElement('img'); img.src = `${backURL}/${orderGoods[0].good.images[0].url}`; img.className = 'imgGood'; divDashboardOrders.appendChild(img); const divName = document.createElement('div'); divName.className = 'divName'; divName.textContent = `${orderGoods[0].good.name}`; divDescriptionOrder.appendChild(divName); const wrapperForCharacteristicGood = document.createElement('div'); wrapperForCharacteristicGood.className = 'wrapperForCharacteristicGood'; const quantityDiv = document.createElement('div'); quantityDiv.textContent = `${orderGoods[0]['count']}шт`; wrapperForCharacteristicGood.appendChild(quantityDiv); const totalDiv = document.createElement('div'); totalDiv.textContent = `${order.total} грн`; totalDiv.className = 'totalDiv'; wrapperForCharacteristicGood.appendChild(totalDiv); divDashboardOrders.appendChild(divDescriptionOrder); divDescriptionOrder.appendChild(wrapperForCharacteristicGood); } }); } } } }); store.subscribe(() => { const [, , _id] = location.hash.split('/'); header.innerHTML = ''; const namCat = document.createElement('div'); namCat.className = 'nameGategory'; namCat.textContent = 'Категории'; const enter = document.createElement('a'); enter.className = 'enterToPrivate'; enter.textContent = 'Вход в кабинет'; enter.href = `#/login/${_id}`; header.appendChild(namCat); header.appendChild(enter); } ); store.subscribe(() => { const [, , _id] = location.hash.split('/'); let wrapperForCart = document.createElement('div'); wrapperForCart.className = 'wrapperForCart'; const link = document.createElement('a'); link.className = 'cart'; header.appendChild(wrapperForCart); wrapperForCart.innerHTML = ` Корзина `; const cartArr = Object.values(store.getState().cart); const counter = cartArr.length === 0 ? 0 : cartArr.reduce((accum, {count}) => accum + count, 0); wrapperForCart.innerHTML = `Корзина ${counter} `; } ); store.subscribe(() => { const {catById} = store.getState().promise; const [, route, _id] = location.hash.split('/'); if (catById?.payload && route === 'category') { const {name} = catById.payload; main.innerHTML = `

${name}

`; if (catById.payload.subCategories) { console.log('тут подкатегории'); for (let good of catById.payload.subCategories) { main.innerHTML += `

${good.name}

`; main.innerHTML += `Подробнее`; } } if (catById.payload.goods) { for (const good of catById.payload.goods) { const {_id, name, price, images} = good; const card = document.createElement('div'); card.className = 'card'; const link = document.createElement('a'); card.innerHTML = `

${name}

${price} грн Подробнее `; const buttonBuy = document.createElement('button'); buttonBuy.className = 'buttonBuy'; buttonBuy.textContent = 'Купить'; buttonBuy.onclick = () => { store.dispatch(actionCartAdd(good, 1)); }; card.appendChild(buttonBuy); aside.append(link); main.append(card); } } } } ); store.subscribe(() => { const {goodById} = store.getState().promise; const [, route, _id] = location.hash.split('/'); if (goodById?.payload && route === 'good') { console.log('я на странице описания товара'); if (location.hash.indexOf(`#/good/${_id}`) !== -1) { main.innerHTML = ''; const {name, price, images, description} = goodById.payload; const card = document.createElement('div'); card.innerHTML = `

${name}

${price}
${description}
`; main.append(card); } } } ); store.subscribe(() => { localStorage.setItem('cart', JSON.stringify(store.getState().cart)); });