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}) const store = createStore(combinedReducer) // console.log(store.getState()) // {promise: {}, auth: {}} 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'}) // const loginStore = createStore(authReducer) store.subscribe(() => console.log(store.getState())) // const inputToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2MWE0ZGIyOWM3NTBjMTJiYTZiYTQwMjIiLCJsb2dpbiI6ImVxd2VxZXdldyIsImFjbCI6WyI2MWE0ZGIyOWM3NTBjMTJiYTZiYTQwMjIiLCJ1c2VyIl19LCJpYXQiOjE2MzgxOTQ1NzZ9.Pi1GO6x7wdNrIrUKCQT-32-SsqmgFY-oFDrrXmw74-8' // loginStore.dispatch(actionAuthLogin(inputToken)) // loginStore.dispatch(actionAuthLogout()) // console.log(store.getState()) 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 promiceStore = createStore(promiseReducer) // store.subscribe(() => console.log(store.getState())) // const delay = (ms) => new Promise((ok) => setTimeout(() => ok(ms), ms)) // // store.dispatch(actionPending('delay1000')) // // delay(1000).then(data => store.dispatch(actionResolved('delay1000', data)), // // error => store.dispatch(actionRejected('delay1000', error))) // // store.dispatch(actionPending('delay2000')) // // delay(2000).then(data => store.dispatch(actionResolved('delay2000', data)), // // error => store.dispatch(actionRejected('delay2000', 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={}) => { // try { 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]] } // } // catch (error) { // console.log('Что-то не так, Бро ' + error); // } } const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua/' const gql = getGQL(backURL + 'graphql'); 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)) } } ) // regBtn.onclick = () => { // store.dispatch(actionFullRegister(loginReg.value, passReg.value)) // } // loginBtn.onclick = () => { // store.dispatch(actionFullLogin(loginInput.value, passInput.value)) // } // logoutBtn.onclick = () => { // store.dispatch(actionAuthLogout()) // } 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}])})) ) // actionGoodById по аналогии 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 = () => ( 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([{}])})) ) store.dispatch(actionRootCats()) store.subscribe(() => { const {promise} = store.getState() const {rootCats} = promise if (rootCats?.payload) { aside.innerHTML = '' const regBtn = document.createElement('a') regBtn.href = `#/register` regBtn.innerText = 'Register' const loginBtn = document.createElement('a') loginBtn.href = `#/login` loginBtn.innerText = 'Login' const logoutBtn = document.createElement('button') logoutBtn.innerText = 'Logout' aside.append(regBtn, loginBtn, logoutBtn) 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) } } }) // location.hash - адресная строка после решетки 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('страница товара') }, register(){ let goRegister = createForm(main, 'Register', actionFullRegister) goRegister() }, login(){ let goLogin = createForm(main, 'Login', actionFullLogin) goLogin() }, orders(){ store.dispatch(actionGoodsByUser()) } } if (route in routes) { routes[route]() } } 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 = '' } } window.onhashchange() store.subscribe(() => { const {promise} = store.getState() const {catById} = promise const [,route, _id] = location.hash.split('/') if (catById?.payload && route === 'category'){ const {name} = catById.payload; main.innerHTML = `

${name}

` if (catById.payload.subCategories) { for(const {_id, name} of catById.payload.subCategories) { const link = document.createElement('a'); link.href = `#/category/${_id}`; link.innerText = name; main.append(link); } } if (catById.payload.goods) { for (const {_id, name, price, images} of catById.payload.goods){ const card = document.createElement('div') card.innerHTML = `

${name}

${price}
Перейти на страницу товара ` main.append(card) } } } }) store.subscribe(() => { const {promise} = store.getState() const {goodById} = promise const [,route, _id] = location.hash.split('/'); if (goodById?.payload && route === 'good') { main.innerHTML = ''; const {_id, name, images, price, description} = goodById.payload; const card = document.createElement('div'); card.innerHTML = `

${name}

${price}

${description}

`; main.append(card); } } //ТУТ ДОЛЖНА БЫТЬ ПРОВЕРКА НА НАЛИЧИЕ goodById в редакс //и проверка на то, что сейчас в адресной строке адрес ВИДА #/good/АЙДИ //в таком случае очищаем main и рисуем информацию про товар с подробностями ) 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?.payload && route === 'orders'){ main.innerHTML = '' 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.innerHTML = `

${name}

Куплено ${count} по ${price} грн. Итого:${total}
Перейти на страницу товара ` main.append(card) } } } } const totalBlock = document.createElement('b') totalBlock.innerText = 'Итого потрачено: ' + totalMoney + ' грн' main.append(totalBlock) } } }) // store.dispatch(actionPromise('delay1000', delay(1000))) // // store.dispatch(actionPromise('delay2000', delay(2000))) // // store.dispatch(actionPromise('luke', fetch('https://swapi.dev/api/people/1/').then(res => res.json())))