|
@@ -0,0 +1,504 @@
|
|
|
+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 }))
|
|
|
+
|
|
|
+
|
|
|
+ /* actionPromise(
|
|
|
+ "register",
|
|
|
+ gql(
|
|
|
+ `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();
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/* 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 = '<img src="Loading_icon.gif">'
|
|
|
+ } 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 = `<h1>${name}</h1>`
|
|
|
+ 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 {
|
|
|
+ debugger
|
|
|
+ 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 = `<h1>${name}</h1>`
|
|
|
+
|
|
|
+ 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 = `<h1>${name}</h1>`
|
|
|
+ // нарисовать картинки, описание, цену и т.д.
|
|
|
+ }
|
|
|
+}) */
|
|
|
+
|
|
|
+window.onhashchange = () => {
|
|
|
+ const [, route, _id] = location.hash.split('/')
|
|
|
+ console.log(route, _id)
|
|
|
+ const routes = {
|
|
|
+ category() {
|
|
|
+ store.dispatch(actionCatById(_id))
|
|
|
+ },
|
|
|
+ good() {
|
|
|
+ store.dispatch(actionGoodById(_id))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (route in routes) {
|
|
|
+ routes[route]()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+window.onhashchange()
|