|
@@ -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>
|