import React, { useState } from "react"; import "./App.css"; import thunk from "redux-thunk"; import { createStore, combineReducers, applyMiddleware } from "redux"; import { Provider, connect } from "react-redux"; 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 = `` } else { if (catById?.payload && route === 'category'){ main.innerHTML = '' const catBody = document.createElement('div') main.append(catBody) const {name} = catById.payload; catBody.innerHTML = `