Browse Source

Project start_

Gennadysht 2 years ago
parent
commit
7d00fbb228

File diff suppressed because it is too large
+ 1033 - 0
package-lock.json


+ 7 - 0
package.json

@@ -3,12 +3,19 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@emotion/react": "^11.10.5",
+    "@emotion/styled": "^11.10.5",
+    "@fontsource/roboto": "^4.5.8",
+    "@mui/icons-material": "^5.11.0",
+    "@mui/material": "^5.11.2",
+    "@mui/styled-engine-sc": "^5.11.0",
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.4.0",
     "@testing-library/user-event": "^13.5.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-scripts": "5.0.1",
+    "styled-components": "^5.3.6",
     "web-vitals": "^2.1.4"
   },
   "scripts": {

+ 15 - 5
src/App.js

@@ -1,9 +1,21 @@
 import logo from './logo.svg';
 import './App.css';
+import { 
+  JqlTests_RootCats, 
+  JqlTests_RetrieveRootCats,
+  JqlTests_AuthLogin,
+  JqlTests_Goods,
+  JqlTests_GoodFindOne,
+  JqlTests_AuthUpsert
+ } from './Tests/test_jql';
 
 function App() {
   return (
-    <div className="App">
+    <JqlTests_RootCats />
+  );
+}
+{/*    <div className="App">
+      
       <header className="App-header">
         <img src={logo} className="App-logo" alt="logo" />
         <p>
@@ -18,8 +30,6 @@ function App() {
           Learn React
         </a>
       </header>
-    </div>
-  );
-}
-
+  </div>
+*/}
 export default App;

+ 50 - 0
src/Tests/test_jql.js

@@ -0,0 +1,50 @@
+import { gqlRootCats, gqlCategoryFindOne } from "../jql/gqlCategories"
+import { actionLogin, actionAuthUpsert } from "../jql/gqlAuth"
+import { gqlGoodFind, gqlGoodFindOne } from "../jql/gqlGoods"
+
+
+
+export  const JqlTests_RootCats = async() => {
+    let result = await gqlRootCats();
+    return <div></div>;
+}
+
+export  const JqlTests_RetrieveRootCats = async() => {
+    let result = await gqlRootCats();
+    for(let cat of result.data.CategoryFind)
+    {
+        let catData = (await gqlCategoryFindOne(cat._id)).data.CategoryFindOne;
+    }
+    return <div></div>;
+}
+
+
+export  const JqlTests_AuthLogin = async() => {
+    let result = (await actionLogin('admin', '123123')).data.login;
+    return <div></div>;
+}
+
+export  const JqlTests_AuthUpsert = async() => {
+    let result = (await actionAuthUpsert('test7', 'test1')).data;
+    console.log("TESTAU " + result);
+    return <div></div>;
+}
+
+const signIn = async (login, password) =>{
+    window.localStorage["AUTH_TOKEN"] = (await actionLogin(login, password)).data.login;
+    return <div></div>;
+}
+
+export  const JqlTests_Goods = async() => {
+    let result = (await gqlGoodFind()).data.GoodFind;
+    return <div></div>;
+}
+
+export  const JqlTests_GoodFindOne = async() => {
+    let goods = await gqlGoodFind();
+    for(let good of goods.data.GoodFind)
+    {
+        let goodData = (await gqlGoodFindOne(good._id)).data.GoodFindOne;
+    }
+    return <div></div>;
+}

+ 10 - 1
src/index.js

@@ -1,9 +1,18 @@
 import React from 'react';
 import ReactDOM from 'react-dom/client';
 import './index.css';
-import App from './App';
+import App from './App.js';
 import reportWebVitals from './reportWebVitals';
+import { 
+  JqlTests_RootCats, 
+  JqlTests_RetrieveRootCats,
+  JqlTests_AuthLogin,
+  JqlTests_Goods,
+  JqlTests_GoodFindOne,
+  JqlTests_AuthUpsert
+ } from './Tests/test_jql';
 
+ JqlTests_AuthUpsert();
 const root = ReactDOM.createRoot(document.getElementById('root'));
 root.render(
   <React.StrictMode>

+ 54 - 0
src/jql/gqlAuth.js

@@ -0,0 +1,54 @@
+import {gql} from "./../utills/gql";
+
+export const actionLogin = (login, password) => {
+    const upsertQuery = `query login($login:String, $password:String){
+                        login(login:$login, password:$password)
+                }`;
+
+    return gql(upsertQuery, { login: login, password: password });
+}
+/*export const actionFullLogin = (login, password) => {
+    return gqlFullLogin = async (dispatch) => {
+        try {
+            delete localStorage.authToken;
+            //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис, 
+            //так как actionPromise возвращает асинхронную функцию
+            let promiseResult = actionLogin(login, password);
+            let res = await promiseResult;
+            if (res && res.data) {
+                let token = Object.values(res.data)[0];
+                if (token && typeof token == 'string')
+                    return dispatch(actionAuthLogin(token));
+                else
+                    addErrorAlert("User not found");
+            }
+        }
+        catch (error) {
+            addErrorAlert(error.message);
+            throw error;
+        }
+        //проверьте что token - строка и отдайте его в actionAuthLogin
+    }
+}*/
+////////////////////////////////////////
+export const actionAuthUpsert = (login, password) => {
+    const loginQuery = `mutation UserRegistration($login: String, $password: String) {
+                            UserUpsert(user: {login: $login, password: $password}) {
+                                _id createdAt
+                            }
+                        }`;
+
+    return gql(loginQuery, { login: login, password: password });////////  
+}
+/*export const actionFullAuthUpsert = (login, password) => {
+    return gqlFullAuthUpsert = async (dispatch) => {
+        //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис, 
+        //так как actionPromise возвращает асинхронную функцию
+        delete localStorage.authToken;
+        let promiseResult = actionAuthUpsert(login, password);
+        let res = await promiseResult;
+        dispatch(actionFullLogin(login, password));
+        console.log(res)
+        //проверьте что token - строка и отдайте его в actionAuthLogin
+    }
+}*/

+ 27 - 0
src/jql/gqlCategories.js

@@ -0,0 +1,27 @@
+import {gql} from "./../utills/gql";
+import {actionPromise} from "./../reducers/promiseReducer";
+
+export const gqlRootCats = () => {
+    const catQuery = `query roots {
+        CategoryFind(query: "[{\\"parent\\": null }]") {
+                                _id name
+                            }}`;
+    return gql(catQuery);
+}
+const actionRootCats = () =>
+    actionPromise('rootCats', gqlRootCats());
+export const gqlCategoryFindOne = (id) => {
+    const catQuery = `query CategoryFindOne($q: String) {
+            CategoryFindOne(query: $q) {
+                _id name
+                parent { _id name }
+                subCategories { _id name }
+                goods { _id name price description 
+                    images { url }
+                }
+            }
+        }`;
+    return gql(catQuery, { q: `[{\"_id\": \"${id}\"}]` });
+}
+const actionCategoryFindOne = (id) =>
+    actionPromise('catFindOne', gqlCategoryFindOne(id));

+ 25 - 0
src/jql/gqlGoods.js

@@ -0,0 +1,25 @@
+import {gql} from "./../utills/gql";
+export const gqlGoodFindOne = (id) => {
+    const catQuery = `
+                query GoodFindOne($q: String) {
+                    GoodFindOne(query: $q) {
+                        _id name  price description
+                        images { url }
+                    }
+                }
+                `;
+    return gql(catQuery, { q: `[{\"_id\": \"${id}\"}]` });
+}
+export const gqlGoodFind = (id) => {
+    const catQuery = `
+                query GoodFind($q: String) {
+                    GoodFind(query: $q) {
+                        _id name  price description
+                        images { url }
+                    }
+                }
+                `;
+    return gql(catQuery, { q: `[{\"name\": \"//\"}]` });
+}
+/*const actionGoodFindOne = (id) =>
+    actionPromise('goodFindOne', gqlGoodFindOne(id));*/

+ 50 - 0
src/jql/gqlOrders.js

@@ -0,0 +1,50 @@
+const orderUpsert = (order, id = null) => {
+    const orderUpsertQuery = `mutation OrderUpsert($order: OrderInput) {
+                            OrderUpsert(order: $order) {
+                                _id
+                            }
+                        }`;
+    return gql(orderUpsertQuery, { order: { "_id": id, "orderGoods": order } });
+}
+const orderFullUpsert = (then) => {
+    return gqlFullOrderUpsert = async (dispatch, getState) => {
+        let state = getState();
+        let order = [];
+        for (cartItem of Object.values(state.cartReducer)) {
+            //{count: 3, good: {_id: "xxxx" }}
+            order.push({ good: { _id: cartItem.good._id }, count: cartItem.count });
+        }
+        if (order.length > 0) {
+            //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис, 
+            //так как actionPromise возвращает асинхронную функцию
+            let promiseResult = orderUpsert(order);
+            let res = await promiseResult;
+            if (res && res.errors && res.errors.length > 0) {
+                addErrorAlert(res.errors[0].message);
+                throw res.errors[0];
+            }
+            dispatch(actionCartClear());
+        }
+        if (then)
+            then();
+        //проверьте что token - строка и отдайте его в actionAuthLogin
+    }
+}
+const gqlFindOrders = () => {
+    const findOrdersQuery = `query OrderFind {
+                            OrderFind(query: "[{}]") {
+                                _id total
+                                orderGoods {
+                                    _id price count total createdAt
+                                    good {
+                                        name 
+                                        images { url }
+                                    }
+                                }
+                            }
+                            }`;
+    return gql(findOrdersQuery);
+}
+const actionFindOrders = () =>
+    actionPromise('orders', gqlFindOrders());
+

+ 29 - 0
src/reducers/authReducer.js

@@ -0,0 +1,29 @@
+export function authReducer(state = {}, action) {                   // диспетчер обработки login
+    if (action) {
+        if (action.type === 'AUTH_LOGIN') {
+            let newState = { ...state };
+            newState.token = action.token;
+            newState.payload = jwtDecode(action.token);
+            if (!newState.payload) {
+                newState.token = undefined;
+            }
+            if (newState.token)
+                localStorage.authToken = newState.token;
+            else
+                delete localStorage.authToken;
+            window.onhashchange();
+            return newState;
+        }
+        else if (action.type === 'AUTH_LOGOUT') {
+            let newState = { ...state };
+            newState.token = undefined;
+            newState.payload = undefined;
+            delete localStorage.authToken;
+            window.onhashchange();
+            return newState;
+        }
+    }
+    return state;
+}
+const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token });
+const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' });

File diff suppressed because it is too large
+ 49 - 0
src/reducers/cartReducer.js


+ 14 - 0
src/reducers/localStoredReducer.js

@@ -0,0 +1,14 @@
+export function localStoredReducer(originalReducer, localStorageKey) {
+    function wrapper(state, action) {
+        if (!state) {     /////проверка на первичность запуска !state
+            try {
+                return JSON.parse(localStorage[localStorageKey]);
+            }
+            catch { }
+        }
+        let res = originalReducer(state, action);
+        localStorage[localStorageKey] = JSON.stringify(res);
+        return res;
+    }
+    return wrapper
+}

+ 29 - 0
src/reducers/promiseReducer.js

@@ -0,0 +1,29 @@
+export function promiseReducer(state = {}, action) {                   // диспетчер обработки
+    if (action) {
+        if (action.type === 'PROMISE') {
+            let newState = { ...state };
+            newState[action.name] = { status: action.status, payload: action.payload, error: action.error };
+            return newState;
+        }
+    }
+    return state;
+}
+const actionPending = (name) => ({ type: 'PROMISE', name: name, status: 'PENDING' });
+const actionFulfilled = (name, payload) => ({ type: 'PROMISE', name: name, payload: payload, status: 'FULFILLED' });
+const actionRejected = (name, error) => ({ type: 'PROMISE', name: name, error: error, status: 'REJECTED' });
+export const actionPromise = (name, promise) => {
+    return /*actionPromiseInt = async (dispatch) => {
+        dispatch(actionPending(name)) //сигнализируем redux, что промис начался
+        try {
+            let payload = await promise //ожидаем промиса
+            if (payload && payload.data)
+                payload = Object.values(payload.data)[0];
+            dispatch(actionFulfilled(name, payload)) //сигнализируем redux, что промис успешно выполнен
+            return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса
+        }
+        catch (error) {
+            console.log(error.message);
+            dispatch(actionRejected(name, error)) //в случае ошибки - сигнализируем redux, что промис несложился
+        }
+    }*/
+}

+ 26 - 0
src/store.js

@@ -0,0 +1,26 @@
+export function createStore(reducer) {
+    let state = reducer(undefined, {})              //стартовая инициализация состояния, запуск редьюсера со state === undefined
+    let cbs = []                                      //массив подписчиков
+
+    const getState = () => { return state; }                   //функция, возвращающая переменную из замыкания
+    const subscribe = cb => (cbs.push(cb),            //запоминаем подписчиков в массиве
+        () => cbs = cbs.filter(c => c !== cb))      //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
+
+    function 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 в объект
+    }
+}
+

+ 89 - 0
src/utills/gql.js

@@ -0,0 +1,89 @@
+const addErrorAlert = (error) => {
+    console.log(`ERROR ${error}`);
+}
+function jwtDecode(token) {                         // расщифровки токена авторизации
+    if (!token || typeof token != "string")
+        return undefined;
+    let tokenArr = token.split(".");
+    if (tokenArr.length != 3)
+        return undefined;
+    try {
+        let tokenJsonStr = atob(tokenArr[1]);
+        let tokenJson = JSON.parse(tokenJsonStr);
+        return tokenJson;
+    }
+    catch (error) {
+        addErrorAlert(error.message);
+        return undefined;
+    }
+}
+function combineReducers(reducers) {
+    function totalReducer(totalState = {}, action) {
+        const newTotalState = {} //объект, который будет хранить только новые состояния дочерних редьюсеров
+
+        //цикл + квадратные скобочки позволяют написать код, который будет работать с любыми количеством дочерных редьюсеров
+        for (const [reducerName, childReducer] of Object.entries(reducers)) {
+            const newState = childReducer(totalState[reducerName], action) //запуск дочернего редьюсера
+            if (newState !== totalState[reducerName]) { //если он отреагировал на action
+                newTotalState[reducerName] = newState //добавляем его в newTotalState
+            }
+        }
+
+        //Универсальная проверка на то, что хотя бы один дочерний редьюсер создал новый стейт:
+        if (Object.values(newTotalState).length) {
+            return { ...totalState, ...newTotalState } //создаем новый общий стейт, накладывая новый стейты дочерних редьюсеров на старые
+        }
+
+        return totalState //если экшен не был понят ни одним из дочерних редьюсеров, возвращаем общий стейт как был.
+    }
+
+    return totalReducer
+}
+
+function getGql(url) {
+    return function gql(query, vars = undefined) {
+        try {
+            let fetchSettings =
+            {
+                method: "POST",
+                headers:
+                {
+                    "Content-Type": "application/json",
+                    "Accept": "application/json"
+                },
+                body: JSON.stringify(
+                    {
+                        query: query,
+                        variables: vars
+                    })
+            };
+            let authToken = window.localStorage.authToken;
+            if (authToken) {
+                fetchSettings.headers["Authorization"] = `Bearer ${authToken}`;
+            }
+            return fetch(url, fetchSettings)
+                .then(res => {
+                    try {
+                        if (!res.ok) {
+                            addErrorAlert(res.statusText);
+                            throw Error(res.statusText);
+                        }
+                        return res.json();
+                    }
+                    catch (error) {
+                        addErrorAlert(error.message);
+                        throw error;
+                    }
+
+                });
+        }
+        catch (error) {
+            addErrorAlert(error.message);
+            throw error;
+        }
+    }
+}
+export const gql = getGql("http://shop-roles.node.ed.asmer.org.ua/graphql");
+
+//export default gql;
+

+ 458 - 0
test.html

@@ -0,0 +1,458 @@
+<head>
+    <Header>MODULE MARKET</Header>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
+        integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" />
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
+        integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
+        crossorigin="anonymous"></script>
+    <link rel="stylesheet" href="https://cdn.reflowhq.com/v2/toolkit.min.css">
+    <link rel="stylesheet" href="index.css">
+    <style>
+        .alert-fixed {
+            position: fixed;
+            top: 0px;
+            left: 0px;
+            width: 100%;
+            z-index: 9999;
+            border-radius: 0px;
+        }
+    </style>
+    <div class="alert-fixed" id="alertsZone"></div>
+</head>
+
+<body>
+    <div class="container-fluid">
+        <nav class="navbar navbar-expand-md">
+            <a class="navbar-brand" href="?#"><img src="./Img/Logo.png" width="140px"></a>
+            <div class="collapse navbar-collapse" id="main-navigation">
+                <ul class="navbar-nav  align-items-center h3">
+                    <li id="loginLink" class="nav-item  d-none">
+                        <a class="nav-link" href="?#/login/">Login</a>
+                    </li>
+                    <li id="regLink" class="nav-item d-none">
+                        <a class="nav-link" href="?#/register/">Register</a>
+                    </li>
+                    <li id="logoutLink" class="nav-item d-none">
+                        <a class="nav-link" href="?#/logout/">Logout</a>
+                    </li>
+                    <li id="cartLink" class="nav-item">
+                        <a class="nav-link" href="?#/cart/">
+                            <i class="fa badge fa-lg" id="cartCountBadge" value=0>
+                                <img src="./Img/корзина.png" width="60px">
+                                <!--<span class="badge badge-light" id="cartCountBadge">0</span>-->
+                            </i>
+                        </a>
+                    </li>
+                    <li id="historyLink" class="nav-item  d-none">
+                        <a class="nav-link" href="#/orders/">History</a>
+                    </li>
+                </ul>
+            </div>
+        </nav>
+    </div>
+
+    <div class="container-fluid">
+        <div class="row">
+            <div class="col-2 left">
+                <div style="background-color: wheat;" class="d-flex flex-column flex-shrink-0 p-3 text-dark">
+                    <ul id="categoriesList" class="nav nav-pills flex-column mb-auto">
+                    </ul>
+                </div>
+            </div>
+            <div class="col-8">
+                <div id="main">
+                </div>
+            </div>
+        </div>
+    </div>
+
+
+    <!--<header>
+        <div>
+            <a href="#/login/">Login</a>
+        </div>
+        <div>
+            <a href="#/logout/">Logout</a>
+        </div>
+        <div>
+            <a href="#/orders/">Orders History</a>
+        </div>
+        <div id='cartIcon'></div>
+        <div>
+            <a href="#/cart">Cart</a>
+        </div>
+    </header>
+    <div id='mainContainer'>
+        <aside id='aside'>
+        </aside>
+        <main id='main'>
+        </main>
+    </div>-->
+    <script type="text/javascript" src="./login.js"></script>
+    <script type="text/javascript" src="./alerts.js"></script>
+    <script type="text/javascript" src="./pagination.js"></script>
+    <script type="module" src="./src/store.js"></script>
+    <script type="module" src="./src/reducers/localStoredReducer.js"></script>
+    <script type="module" src="./src/utills/gql.js"></script>
+    <script type="module" >
+        import {createStore} from "./src/store.js";
+        const addErrorAlert = (error) => {
+            alert(error);
+        }
+        const store = createStore(combineReducers({ promiseReducer, authReducer, cartReducer: localStoredReducer(cartReducer, 'cart') }));
+        const delay = (ms, action) => new Promise(ok => setTimeout(() => {
+            action();
+            ok(ms);
+        }, ms));
+        store.subscribe(() => {
+            let state = store.getState();
+            let cartItemsCount = 0;
+            if (state.cartReducer) {
+                let cartItems = Object.values(state.cartReducer);
+                for (item of  Object.values(state.cartReducer))
+                     cartItemsCount += item.count;
+            }
+            cartCountBadge.setAttribute("value", cartItemsCount);
+            console.log({ state: store.getState() });
+        });
+        //////////////////////////////////////////////
+        const addToCartBtn = (htmlEl, good) => {
+            let btn = document.createElement("button");
+            btn.innerText = "Add to Cart";
+            btn.classList.add("ref-button");
+            btn.classList.add("preview-toggle");
+            btn.onclick = () => {
+                store.dispatch(actionCartAdd(good));
+            };
+            htmlEl.appendChild(btn);
+            return btn;
+        }
+
+        const fillRootCategories = (categories, htmlEl) => {
+            htmlEl.innerText = '';
+            if (!categories)
+                return;
+            let innerHtml = '';
+            for (category of categories) {
+                innerHtml += `<li class="h3"><a href="#/categories/${category._id}" class="nav-link text-dark">${category.name}</a></li>`;
+            }
+            htmlEl.innerHTML = innerHtml;
+        }
+        store.subscribe(() =>
+            subscribePromiseItem("rootCats", categoriesList, [], fillRootCategories));
+
+        const fillCurrentCategoryContent = (category, htmlEl) => {
+            htmlEl.innerHTML = '';
+            const { name, parent, subCategories, goods } = category;
+            htmlEl.innerHTML = `<h1>${name}</h1>
+                                ${parent?.name ? `<section>Parent: <a href="#/subCategories/${parent?._id}">${parent.name}</a></section>` : ''}
+                             `
+            if (subCategories?.length > 0) {
+                htmlEl.innerHTML += '';//`<h3>Sub Categories:</h3><br>`
+                for (const subCategory of subCategories) {
+                    htmlEl.innerHTML += `<h4><a href="#/subCategories/${subCategory._id}">${subCategory.name}</a><h4><hr/>`
+                }
+            }
+            htmlEl.innerHTML += '';//`<section>Products:</section><br>`;
+            addProductsList(goods, htmlEl)
+        }
+        store.subscribe(() =>
+            subscribePromiseItem(
+                "catFindOne", main, ["categories", "subCategories"], fillCurrentCategoryContent));
+
+
+        const addProductsList = (goods, htmlEl) => {
+            let outerDiv = document.createElement("div");
+            outerDiv.innerHTML =
+                `
+                    <div class="reflow-product-list">
+                        <div id="productsList" class="ref-products">
+                        </div>
+                        <div id="productsListPagination">
+                        </div>
+                    </div>
+                `;
+
+            htmlEl.appendChild(outerDiv);
+            for (good of goods) {
+                addProductToList(good, productsList);
+            }
+            new Pagination(productsList, productsListPagination, 5, listItemTag = 'div.ref-product')
+        }
+        const addProductToList = (good, htmlEl) => {
+            let outerDiv = document.createElement('div');
+            const { name, _id, price, description, images } = good;
+            outerDiv.innerHTML =
+                `
+                    <div id="good_${_id}" class="ref-product d-none">
+                        <div class="ref-media"><img class="ref-image"
+                                src="${getFullImageUrl(good.images[0])}" loading="lazy" /></div>
+                        <div class="ref-product-data">
+                            <div class="ref-product-info">
+                                <h5 class="ref-name"><a href="#/goods/${_id}">${name}</a></h5>
+                                <p class="ref-excerpt">${description}</p>
+                            </div><strong class="ref-price">$${price}</strong>
+                        </div>
+                        <div id="addCartDiv_${_id}" class="ref-addons"></div>
+                    </div>
+                `;
+            htmlEl.appendChild(outerDiv);
+            let addCartDiv = document.getElementById(`addCartDiv_${_id}`);
+            addToCartBtn(addCartDiv, good);
+        }
+
+        const fillCurrentGoodContent = (good, htmlEl) => {
+            htmlEl.innerHTML = '';
+            const { name, _id, price, description, images } = good;
+            htmlEl.innerHTML = `<h1>${name}</h1>
+                                <section>Description: ${description}</section>
+                                <section>Price: ${price}</section>
+                             `;
+            htmlEl.innerHTML += `<section>Images:</section><br>`  //вставить css display block
+            for (const image of images) {
+                htmlEl.innerHTML += `<img width="170px" src="${"http://shop-roles.node.ed.asmer.org.ua/"}${image.url}"</img><br>`//вставить css display block
+            }
+            addToCartBtn(htmlEl, good);
+        }
+        store.subscribe(() =>
+            subscribePromiseItem(
+                "goodFindOne", main, ["goods"], fillCurrentGoodContent));
+
+        const subscribePromiseItem = (promiseName, htmlEl, subscrNames, execFunc) => {
+            const [, route, _id] = location.hash.split('/');
+            if ((subscrNames.length > 0 && (!route || !subscrNames.some(v => v == route)))/* || !_id*/)
+                return;
+            let reducerData = store.getState().promiseReducer[promiseName];
+            if (!reducerData)
+                return;
+            const { status, payload, error } = reducerData;
+            if (status === 'PENDING') {
+                htmlEl.innerHTML = `<img src='https://cdn.dribbble.com/users/63485/screenshots/1309731/infinite-gif-preloader.gif' />`
+            }
+            else if (status == "FULFILLED") {
+                execFunc(payload, htmlEl);
+            }
+            else if (status == "FULFILLED") {
+                addErrorAlert(error.message);
+            }
+        }
+
+        const getFullImageUrl = (image) =>
+            `http://shop-roles.node.ed.asmer.org.ua/${image?.url}`;
+
+        const showCartContent = (cart, htmlEl) => {
+            htmlEl.innerHTML = '';
+            htmlEl.innerHTML = `<h1>Cart</h1>
+                               `;
+            htmlEl.innerHTML += '';//`<section>Items:</section><br>`  //вставить css display block
+            let allCount = 0;
+            let htmlContent = '';
+            for (const item of Object.values(cart)) {
+                let { count, good } = item;
+                let inpId = `inp_${good._id}`;
+                let delBtnId = `delBtn_${good._id}`;
+                htmlContent += `
+                    <div>
+                        <img width="170px" src="${getFullImageUrl(good.images[0])}"</img>
+                        <a href="#/goods/${good._id}">${good.name}</a>
+                        <input type="number" min="1" max="999" id="${inpId}" value="${count}">
+                        <button class="ref-button preview-toggle" id="${delBtnId}">Remove</button>
+                    </div>
+                    <br>`//вставить css display block
+                allCount += count;
+            }
+            htmlContent += `<div>Count ${allCount}</div><br>`;
+            htmlContent += `<button class="ref-button preview-toggle" id="btnCheckout">Checkout</button><br>`;
+            htmlEl.innerHTML += htmlContent;
+            for (const item of Object.values(cart)) {
+                let { good } = item;
+                let inpId = `inp_${good._id}`;
+                let delBtnId = `delBtn_${good._id}`;
+                let inp = document.getElementById(inpId);
+                inp.addEventListener("change", function (e) { store.dispatch(actionCartSet(good, +inp.value)); });
+                let delBtn = document.getElementById(delBtnId);
+                delBtn.addEventListener("click", function (e) { store.dispatch(actionCartDel(good)); });
+            }
+            let btnCheckout = document.getElementById("btnCheckout");
+            btnCheckout.addEventListener("click", function (e) {
+                window.location = "#/checkout/";
+            });
+        }
+        store.subscribe(() =>
+            subscribeSimple(
+                "cartReducer", main, ["cart"], showCartContent));
+
+
+        const showOrder = (order, num, htmlEl) => {
+            let htmlContent = `<div class="order d-none"><h2>Order: #${num}</h2>
+                                <!--<div>Created on: ${order.createdAt}</div>-->
+                               `;
+            htmlContent += `<h3>Items:</h3>`  //вставить css display block
+            let orderGoods = Object.values(order.orderGoods);
+            for (let i = 0; i < orderGoods.length; i++) {
+                let { order, count, price, total, good } = orderGoods[i];
+                htmlContent +=
+                    `
+                        <div class="ref-product align-items-center">
+                            <strong class="ref-count">${i}.   </strong>
+                            <div class="ref-media"><img class="ref-image" width="170px" src="${getFullImageUrl(good.images[0])}"</img></div>
+                            <div class="ref-product-data">
+                                <div class="ref-product-info">
+                                    <h5 class="ref-name"><a href="#/goods/${good._id}">${good.name}</a></h5>
+                                </div>
+                                <strong class="ref-price">Price: ${price}</strong>
+                                <strong class="ref-count">Count: ${count}</strong>
+                                <strong class="ref-price">Total: ${total}</strong>
+                            </div>
+                        </div>
+                    `//вставить css display block
+            }
+            htmlContent += `<h3 class="ref-count">Total ${order.total}</h3><hr/></div>`;
+            htmlEl.innerHTML += htmlContent;
+        }
+        const showOrders = (orders, htmlEl) => {
+            htmlEl.innerHTML = "<header>Orders:</header>";
+            if (!localStorage?.authToken)
+                return;
+            if (orders) {
+                let ordersContainerDiv = document.createElement("div");
+                ordersContainerDiv.classList.add("container", "reflow-product-list");
+                let ordersDiv = document.createElement("div");
+                ordersContainerDiv.appendChild(ordersDiv);
+                ordersDiv.classList.add("ref-products");
+                for (let i = orders.length; i > 0; i--) {
+                    let order = orders[i - 1];
+                    showOrder(order, i, ordersDiv);
+                }
+                let paginationDiv = document.createElement("div");
+                htmlEl.append(ordersContainerDiv, paginationDiv);
+                new Pagination(ordersDiv, paginationDiv, 5, listItemTag = 'div.order')
+            }
+        }
+        store.subscribe(() =>
+            subscribePromiseItem(
+                "orders", main, ["orders"], showOrders));
+
+        const subscribeSimple = (reducerName, htmlEl, subscrNames, execFunc) => {
+            const [, route, _id] = location.hash.split('/');
+            if (!subscrNames || !subscrNames.some(v => v == route))
+                return;
+            let reducerData = store.getState()[reducerName];
+            execFunc(reducerData, htmlEl);
+        }
+
+        window.onhashchange = () => {
+            const [, route, _id] = location.hash.split('/')
+
+            const routes = {
+                categories() {
+                    console.log('Category', _id)
+                    store.dispatch(actionCategoryFindOne(_id))
+                },
+                subCategories() {
+                    console.log('subCategory', _id)
+                    store.dispatch(actionCategoryFindOne(_id))
+                },
+                goods() {
+                    console.log('good', _id)
+                    store.dispatch(actionGoodFindOne(_id))
+                },
+                cart() {
+                    console.log('cart')
+                    store.dispatch(actionCartShow())
+                },
+                checkout() {
+                    console.log('checkout');
+                    let state = store.getState();
+                    if (routes.login())
+                        store.dispatch(orderFullUpsert(() => window.location = "#/orders/"));
+                },
+                orders() {
+                    if (!localStorage.authToken) {
+                        showOrders([], main);
+                        return;
+                    }
+                    console.log('order');
+                    store.dispatch(actionFindOrders());
+                },
+                login() {
+                    if (!localStorage.authToken) {
+                        main.innerText = '';
+                        const form = new LoginForm(main);
+                        form.onLogin = (login, password) => {
+                            store.dispatch(actionFullLogin(login, password));
+                        }
+                        return false;
+                    }
+                    else if (route == "login") {
+                        window.location = "#/";
+                        window.location.reload();
+                    }
+                    return true;
+                },
+                register() {
+                    if (!localStorage.authToken) {
+                        main.innerText = '';
+                        const form = new LoginForm(main);
+                        form.onLogin = (login, password) => {
+                            store.dispatch(actionFullAuthUpsert(login, password));
+                        }
+                        return false;
+                    }
+                    else if (route == "register") {
+                        window.location = "#/";
+                        window.location.reload();
+                    }
+                    return true;
+                },
+                logout() {
+                    if (localStorage.authToken) {
+                        store.dispatch(actionAuthLogout());
+                        window.location = "#/login/";
+                        window.location.reload();
+                    }
+                },
+                //register(){
+                ////нарисовать форму регистрации, которая по нажатию кнопки Login делает store.dispatch(actionFullRegister(login, password))
+                //},
+            }
+
+            if (localStorage.authToken) {
+                loginLink.classList.add('d-none');
+                regLink.classList.add('d-none');
+                historyLink.classList.remove('d-none');
+                logoutLink.classList.remove('d-none');
+            }
+            else {
+                loginLink.classList.remove('d-none');
+                regLink.classList.remove('d-none');
+                historyLink.classList.add('d-none');
+                logoutLink.classList.add('d-none');
+            }
+
+            if (route in routes) {
+                routes[route]()
+            }
+        }
+
+        window.onhashchange()
+
+        store.dispatch(actionRootCats());
+
+        /*store.dispatch(actionCategoryFindOne("6262ca7dbf8b206433f5b3d1"));
+        store.dispatch(actionGoodFindOne("62d3099ab74e1f5f2ec1a125"));
+        store.dispatch(actionFullLogin("Berg", "123456789"));
+        //store.dispatch(actionFullAuthUpsert("Berg1", "12345678911"));
+        store.dispatch(actionCartAdd({ _id: '62d30938b74e1f5f2ec1a124', price: 50 }));
+
+        delay(3000, () => {
+            store.dispatch(orderFullUpsert());
+            store.dispatch(actionFindOrders());
+        });*/
+
+        //delay(500, () => store.dispatch(actionFindOrders()));
+
+    </script>
+</body>