index.html 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. <head>
  2. <Header>MODULE MARKET</Header>
  3. <meta charset="utf-8" />
  4. <meta name="viewport" content="width=device-width, initial-scale=1" />
  5. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
  6. integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" />
  7. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
  8. integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
  9. crossorigin="anonymous"></script>
  10. <link rel="stylesheet" href="https://cdn.reflowhq.com/v2/toolkit.min.css">
  11. <link rel="stylesheet" href="index.css">
  12. <style>
  13. .alert-fixed {
  14. position: fixed;
  15. top: 0px;
  16. left: 0px;
  17. width: 100%;
  18. z-index: 9999;
  19. border-radius: 0px;
  20. }
  21. </style>
  22. <div class="alert-fixed" id="alertsZone"></div>
  23. </head>
  24. <body>
  25. <div class="container-fluid">
  26. <nav class="navbar navbar-expand-md">
  27. <a class="navbar-brand" href="?#"><img src="./Img/Logo.png" width="140px"></a>
  28. <div class="collapse navbar-collapse" id="main-navigation">
  29. <ul class="navbar-nav align-items-center h3">
  30. <li id="loginLink" class="nav-item d-none">
  31. <a class="nav-link" href="?#/login/">Login</a>
  32. </li>
  33. <li id="regLink" class="nav-item d-none">
  34. <a class="nav-link" href="?#/register/">Register</a>
  35. </li>
  36. <li id="logoutLink" class="nav-item d-none">
  37. <a class="nav-link" href="?#/logout/">Logout</a>
  38. </li>
  39. <li id="cartLink" class="nav-item">
  40. <a class="nav-link" href="?#/cart/">
  41. <i class="fa badge fa-lg" id="cartCountBadge" value=0>
  42. <img src="./Img/корзина.png" width="60px">
  43. <!--<span class="badge badge-light" id="cartCountBadge">0</span>-->
  44. </i>
  45. </a>
  46. </li>
  47. <li id="historyLink" class="nav-item d-none">
  48. <a class="nav-link" href="#/orders/">History</a>
  49. </li>
  50. </ul>
  51. </div>
  52. </nav>
  53. </div>
  54. <div class="container-fluid">
  55. <div class="row">
  56. <div class="col-2 left">
  57. <div style="background-color: wheat;" class="d-flex flex-column flex-shrink-0 p-3 text-dark">
  58. <ul id="categoriesList" class="nav nav-pills flex-column mb-auto">
  59. </ul>
  60. </div>
  61. </div>
  62. <div class="col-8">
  63. <div id="main">
  64. </div>
  65. </div>
  66. </div>
  67. </div>
  68. <!--<header>
  69. <div>
  70. <a href="#/login/">Login</a>
  71. </div>
  72. <div>
  73. <a href="#/logout/">Logout</a>
  74. </div>
  75. <div>
  76. <a href="#/orders/">Orders History</a>
  77. </div>
  78. <div id='cartIcon'></div>
  79. <div>
  80. <a href="#/cart">Cart</a>
  81. </div>
  82. </header>
  83. <div id='mainContainer'>
  84. <aside id='aside'>
  85. </aside>
  86. <main id='main'>
  87. </main>
  88. </div>-->
  89. <script type="text/javascript" src="./login.js"></script>
  90. <script type="text/javascript" src="./alerts.js"></script>
  91. <script type="text/javascript" src="./pagination.js"></script>
  92. <script>
  93. const addErrorAlert = (error) => {
  94. addAlert(alertsZone, error, 'warning');
  95. }
  96. function jwtDecode(token) { // расщифровки токена авторизации
  97. if (!token || typeof token != "string")
  98. return undefined;
  99. let tokenArr = token.split(".");
  100. if (tokenArr.length != 3)
  101. return undefined;
  102. try {
  103. let tokenJsonStr = atob(tokenArr[1]);
  104. let tokenJson = JSON.parse(tokenJsonStr);
  105. return tokenJson;
  106. }
  107. catch (error) {
  108. addErrorAlert(error.message);
  109. return undefined;
  110. }
  111. }
  112. function combineReducers(reducers) {
  113. function totalReducer(totalState = {}, action) {
  114. const newTotalState = {} //объект, который будет хранить только новые состояния дочерних редьюсеров
  115. //цикл + квадратные скобочки позволяют написать код, который будет работать с любыми количеством дочерных редьюсеров
  116. for (const [reducerName, childReducer] of Object.entries(reducers)) {
  117. const newState = childReducer(totalState[reducerName], action) //запуск дочернего редьюсера
  118. if (newState !== totalState[reducerName]) { //если он отреагировал на action
  119. newTotalState[reducerName] = newState //добавляем его в newTotalState
  120. }
  121. }
  122. //Универсальная проверка на то, что хотя бы один дочерний редьюсер создал новый стейт:
  123. if (Object.values(newTotalState).length) {
  124. return { ...totalState, ...newTotalState } //создаем новый общий стейт, накладывая новый стейты дочерних редьюсеров на старые
  125. }
  126. return totalState //если экшен не был понят ни одним из дочерних редьюсеров, возвращаем общий стейт как был.
  127. }
  128. return totalReducer
  129. }
  130. function cartReducer(state = {}, action) { // диспетчер обработки
  131. switch (action.type) {
  132. case 'CART_ADD':
  133. if (action.count >= 0) {
  134. let newState = { ...state };
  135. let { count } = state[action.good._id] ?? { count: 0 };
  136. newState[action.good._id] = { count: action.count + count, good: { ...action.good } }
  137. return newState;
  138. }
  139. case 'CART_SUB':
  140. if (action.count >= 0) {
  141. let newState = { ...state };
  142. let { count } = state[action.good._id] ?? { count: 0 };
  143. if (count >= action.count) {
  144. newState[action.good._id] = { count: action.count - count, good: { ...action.good } }
  145. return newState;
  146. }
  147. }
  148. break;
  149. case 'CART_DEL':
  150. {
  151. let newState = { ...state };
  152. delete newState[action.good._id];
  153. return newState;
  154. }
  155. case 'CART_SET':
  156. {
  157. let newState = { ...state };
  158. newState[action.good._id] = { count: action.count, good: { ...action.good } };
  159. return newState;
  160. }
  161. case 'CART_SHOW':
  162. {
  163. return newState = { ...state };
  164. }
  165. case 'CART_CLEAR':
  166. return {};
  167. }
  168. return state;
  169. }
  170. function localStoredReducer(originalReducer, localStorageKey) {
  171. function wrapper(state, action) {
  172. if (!state) { /////проверка на первичность запуска !state
  173. try {
  174. return JSON.parse(localStorage[localStorageKey]);
  175. }
  176. catch { }
  177. }
  178. let res = originalReducer(state, action);
  179. localStorage[localStorageKey] = JSON.stringify(res);
  180. return res;
  181. }
  182. return wrapper
  183. }
  184. function promiseReducer(state = {}, action) { // диспетчер обработки
  185. if (action) {
  186. if (action.type === 'PROMISE') {
  187. let newState = { ...state };
  188. newState[action.name] = { status: action.status, payload: action.payload, error: action.error };
  189. return newState;
  190. }
  191. }
  192. return state;
  193. }
  194. function authReducer(state = {}, action) { // диспетчер обработки login
  195. if (action) {
  196. if (action.type === 'AUTH_LOGIN') {
  197. let newState = { ...state };
  198. newState.token = action.token;
  199. newState.payload = jwtDecode(action.token);
  200. if (!newState.payload) {
  201. newState.token = undefined;
  202. }
  203. if (newState.token)
  204. localStorage.authToken = newState.token;
  205. else
  206. delete localStorage.authToken;
  207. window.onhashchange();
  208. return newState;
  209. }
  210. else if (action.type === 'AUTH_LOGOUT') {
  211. let newState = { ...state };
  212. newState.token = undefined;
  213. newState.payload = undefined;
  214. delete localStorage.authToken;
  215. window.onhashchange();
  216. return newState;
  217. }
  218. }
  219. return state;
  220. }
  221. function createStore(reducer) {
  222. let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
  223. let cbs = [] //массив подписчиков
  224. const getState = () => { return state; } //функция, возвращающая переменную из замыкания
  225. const subscribe = cb => (cbs.push(cb), //запоминаем подписчиков в массиве
  226. () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
  227. function dispatch(action) {
  228. if (typeof action === 'function') { //если action - не объект, а функция
  229. return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
  230. }
  231. const newState = reducer(state, action) //пробуем запустить редьюсер
  232. if (newState !== state) { //проверяем, смог ли редьюсер обработать action
  233. state = newState //если смог, то обновляем state
  234. for (let cb of cbs) cb() //и запускаем подписчиков
  235. }
  236. }
  237. return {
  238. getState, //добавление функции getState в результирующий объект
  239. dispatch,
  240. subscribe //добавление subscribe в объект
  241. }
  242. }
  243. function getGql(url) {
  244. return function gql(query, vars = undefined) {
  245. try {
  246. let fetchSettings =
  247. {
  248. method: "POST",
  249. headers:
  250. {
  251. "Content-Type": "application/json",
  252. "Accept": "application/json"
  253. },
  254. body: JSON.stringify(
  255. {
  256. query: query,
  257. variables: vars
  258. })
  259. };
  260. let authToken = window.localStorage.authToken;
  261. if (authToken) {
  262. fetchSettings.headers["Authorization"] = `Bearer ${authToken}`;
  263. }
  264. return fetch(url, fetchSettings)
  265. .then(res => {
  266. try {
  267. if (!res.ok) {
  268. addErrorAlert(res.statusText);
  269. throw Error(res.statusText);
  270. }
  271. return res.json();
  272. }
  273. catch (error) {
  274. addErrorAlert(error.message);
  275. throw error;
  276. }
  277. });
  278. }
  279. catch (error) {
  280. addErrorAlert(error.message);
  281. throw error;
  282. }
  283. }
  284. }
  285. const gql = getGql("http://shop-roles.node.ed.asmer.org.ua/graphql");
  286. const actionPromise = (name, promise) => {
  287. return actionPromiseInt = async (dispatch) => {
  288. dispatch(actionPending(name)) //сигнализируем redux, что промис начался
  289. try {
  290. let payload = await promise //ожидаем промиса
  291. if (payload && payload.data)
  292. payload = Object.values(payload.data)[0];
  293. dispatch(actionFulfilled(name, payload)) //сигнализируем redux, что промис успешно выполнен
  294. return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса
  295. }
  296. catch (error) {
  297. addErrorAlert(error.message);
  298. dispatch(actionRejected(name, error)) //в случае ошибки - сигнализируем redux, что промис несложился
  299. }
  300. }
  301. }
  302. const actionPending = (name) => ({ type: 'PROMISE', name: name, status: 'PENDING' });
  303. const actionFulfilled = (name, payload) => ({ type: 'PROMISE', name: name, payload: payload, status: 'FULFILLED' });
  304. const actionRejected = (name, error) => ({ type: 'PROMISE', name: name, error: error, status: 'REJECTED' });
  305. const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token });
  306. const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' });
  307. const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', count: count, good: good });
  308. const actionCartSub = (good, count = 1) => ({ type: 'CART_SUB', count, good }); //Уменьшение количества товара. Должен уменьшать количество товара в state, или удалять его если количество будет 0 или отрицательным
  309. const actionCartDel = (good) => ({ type: 'CART_DEL', good }); //Удаление товара. Должен удалять ключ из state
  310. const actionCartSet = (good, count = 1) => ({ type: 'CART_SET', count, good }); //Задание количества товара. В отличие от добавления и уменьшения, не учитывает того количества, которое уже было в корзине, а тупо назначает количество поверху (или создает новый ключ, если в корзине товара не было). Если count 0 или отрицательное число - удаляем ключ из корзины;
  311. const actionCartShow = (good, count = 1) => ({ type: 'CART_SHOW', count, good }); //Задание количества товара. В отличие от добавления и уменьшения, не учитывает того количества, которое уже было в корзине, а тупо назначает количество поверху (или создает новый ключ, если в корзине товара не было). Если count 0 или отрицательное число - удаляем ключ из корзины;
  312. const actionCartClear = () => ({ type: 'CART_CLEAR' }); //Очистка корзины. state должен стать пустым объектом {}
  313. ///////////////////////////////////////
  314. const gqlRootCats = () => {
  315. const catQuery = `query roots {
  316. CategoryFind(query: "[{\\"parent\\": null }]") {
  317. _id name
  318. }}`;
  319. return gql(catQuery);
  320. }
  321. const actionRootCats = () =>
  322. actionPromise('rootCats', gqlRootCats());
  323. const gqlCategoryFindOne = (id) => {
  324. const catQuery = `query CategoryFindOne($q: String) {
  325. CategoryFindOne(query: $q) {
  326. _id name
  327. parent { _id name }
  328. subCategories { _id name }
  329. goods { _id name price description
  330. images { url }
  331. }
  332. }
  333. }`;
  334. return gql(catQuery, { q: `[{\"_id\": \"${id}\"}]` });
  335. }
  336. const actionCategoryFindOne = (id) =>
  337. actionPromise('catFindOne', gqlCategoryFindOne(id));
  338. const gqlGoodFindOne = (id) => {
  339. const catQuery = `
  340. query GoodFindOne($q: String) {
  341. GoodFindOne(query: $q) {
  342. _id name price description
  343. images { url }
  344. }
  345. }
  346. `;
  347. return gql(catQuery, { q: `[{\"_id\": \"${id}\"}]` });
  348. }
  349. const actionGoodFindOne = (id) =>
  350. actionPromise('goodFindOne', gqlGoodFindOne(id));
  351. //////////////////////////////////
  352. const actionLogin = (login, password) => {
  353. const upsertQuery = `query login($login:String, $password:String){
  354. login(login:$login, password:$password)
  355. }`;
  356. return gql(upsertQuery, { login: login, password: password });
  357. }
  358. const actionFullLogin = (login, password) => {
  359. return gqlFullLogin = async (dispatch) => {
  360. try {
  361. delete localStorage.authToken;
  362. //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис,
  363. //так как actionPromise возвращает асинхронную функцию
  364. let promiseResult = actionLogin(login, password);
  365. let res = await promiseResult;
  366. if (res && res.data) {
  367. let token = Object.values(res.data)[0];
  368. if (token && typeof token == 'string')
  369. return dispatch(actionAuthLogin(token));
  370. else
  371. addErrorAlert("User not found");
  372. }
  373. }
  374. catch (error) {
  375. addErrorAlert(error.message);
  376. throw error;
  377. }
  378. //проверьте что token - строка и отдайте его в actionAuthLogin
  379. }
  380. }
  381. ////////////////////////////////////////
  382. const actionAuthUpsert = (login, password) => {
  383. const loginQuery = `mutation UserRegistration($login: String, $password: String) {
  384. UserUpsert(user: {login: $login, password: $password}) {
  385. _id createdAt
  386. }
  387. }`;
  388. return gql(loginQuery, { login: login, password: password });////////
  389. }
  390. const actionFullAuthUpsert = (login, password) => {
  391. return gqlFullAuthUpsert = async (dispatch) => {
  392. //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис,
  393. //так как actionPromise возвращает асинхронную функцию
  394. delete localStorage.authToken;
  395. let promiseResult = actionAuthUpsert(login, password);
  396. let res = await promiseResult;
  397. dispatch(actionFullLogin(login, password));
  398. console.log(res)
  399. //проверьте что token - строка и отдайте его в actionAuthLogin
  400. }
  401. }
  402. ////////////////////////////////////////
  403. const orderUpsert = (order, id = null) => {
  404. const orderUpsertQuery = `mutation OrderUpsert($order: OrderInput) {
  405. OrderUpsert(order: $order) {
  406. _id
  407. }
  408. }`;
  409. return gql(orderUpsertQuery, { order: { "_id": id, "orderGoods": order } });
  410. }
  411. const orderFullUpsert = (then) => {
  412. return gqlFullOrderUpsert = async (dispatch, getState) => {
  413. let state = getState();
  414. let order = [];
  415. for (cartItem of Object.values(state.cartReducer)) {
  416. //{count: 3, good: {_id: "xxxx" }}
  417. order.push({ good: { _id: cartItem.good._id }, count: cartItem.count });
  418. }
  419. if (order.length > 0) {
  420. //dispatch возвращает то, что вернул thunk, возвращаемый actionLogin, а там промис,
  421. //так как actionPromise возвращает асинхронную функцию
  422. let promiseResult = orderUpsert(order);
  423. let res = await promiseResult;
  424. if (res && res.errors && res.errors.length > 0) {
  425. addErrorAlert(res.errors[0].message);
  426. throw res.errors[0];
  427. }
  428. dispatch(actionCartClear());
  429. }
  430. if (then)
  431. then();
  432. //проверьте что token - строка и отдайте его в actionAuthLogin
  433. }
  434. }
  435. const gqlFindOrders = () => {
  436. const findOrdersQuery = `query OrderFind {
  437. OrderFind(query: "[{}]") {
  438. _id total
  439. orderGoods {
  440. _id price count total createdAt
  441. good {
  442. name
  443. images { url }
  444. }
  445. }
  446. }
  447. }`;
  448. return gql(findOrdersQuery);
  449. }
  450. const actionFindOrders = () =>
  451. actionPromise('orders', gqlFindOrders());
  452. ///////////////////////////////////////////////////
  453. const store = createStore(combineReducers({ promiseReducer, authReducer, cartReducer: localStoredReducer(cartReducer, 'cart') }));
  454. const delay = (ms, action) => new Promise(ok => setTimeout(() => {
  455. action();
  456. ok(ms);
  457. }, ms));
  458. store.subscribe(() => {
  459. let state = store.getState();
  460. let cartItemsCount = 0;
  461. if (state.cartReducer) {
  462. let cartItems = Object.values(state.cartReducer);
  463. for (item of Object.values(state.cartReducer))
  464. cartItemsCount += item.count;
  465. }
  466. cartCountBadge.setAttribute("value", cartItemsCount);
  467. console.log({ state: store.getState() });
  468. });
  469. //////////////////////////////////////////////
  470. const addToCartBtn = (htmlEl, good) => {
  471. let btn = document.createElement("button");
  472. btn.innerText = "Add to Cart";
  473. btn.classList.add("ref-button");
  474. btn.classList.add("preview-toggle");
  475. btn.onclick = () => {
  476. store.dispatch(actionCartAdd(good));
  477. };
  478. htmlEl.appendChild(btn);
  479. return btn;
  480. }
  481. const fillRootCategories = (categories, htmlEl) => {
  482. htmlEl.innerText = '';
  483. if (!categories)
  484. return;
  485. let innerHtml = '';
  486. for (category of categories) {
  487. innerHtml += `<li class="h3"><a href="#/categories/${category._id}" class="nav-link text-dark">${category.name}</a></li>`;
  488. }
  489. htmlEl.innerHTML = innerHtml;
  490. }
  491. store.subscribe(() =>
  492. subscribePromiseItem("rootCats", categoriesList, [], fillRootCategories));
  493. const fillCurrentCategoryContent = (category, htmlEl) => {
  494. htmlEl.innerHTML = '';
  495. const { name, parent, subCategories, goods } = category;
  496. htmlEl.innerHTML = `<h1>${name}</h1>
  497. ${parent?.name ? `<section>Parent: <a href="#/subCategories/${parent?._id}">${parent.name}</a></section>` : ''}
  498. `
  499. if (subCategories?.length > 0) {
  500. htmlEl.innerHTML += '';//`<h3>Sub Categories:</h3><br>`
  501. for (const subCategory of subCategories) {
  502. htmlEl.innerHTML += `<h4><a href="#/subCategories/${subCategory._id}">${subCategory.name}</a><h4><hr/>`
  503. }
  504. }
  505. htmlEl.innerHTML += '';//`<section>Products:</section><br>`;
  506. addProductsList(goods, htmlEl)
  507. }
  508. store.subscribe(() =>
  509. subscribePromiseItem(
  510. "catFindOne", main, ["categories", "subCategories"], fillCurrentCategoryContent));
  511. const addProductsList = (goods, htmlEl) => {
  512. let outerDiv = document.createElement("div");
  513. outerDiv.innerHTML =
  514. `
  515. <div class="reflow-product-list">
  516. <div id="productsList" class="ref-products">
  517. </div>
  518. <div id="productsListPagination">
  519. </div>
  520. </div>
  521. `;
  522. htmlEl.appendChild(outerDiv);
  523. for (good of goods) {
  524. addProductToList(good, productsList);
  525. }
  526. new Pagination(productsList, productsListPagination, 5, listItemTag = 'div.ref-product')
  527. }
  528. const addProductToList = (good, htmlEl) => {
  529. let outerDiv = document.createElement('div');
  530. const { name, _id, price, description, images } = good;
  531. outerDiv.innerHTML =
  532. `
  533. <div id="good_${_id}" class="ref-product d-none">
  534. <div class="ref-media"><img class="ref-image"
  535. src="${getFullImageUrl(good.images[0])}" loading="lazy" /></div>
  536. <div class="ref-product-data">
  537. <div class="ref-product-info">
  538. <h5 class="ref-name"><a href="#/goods/${_id}">${name}</a></h5>
  539. <p class="ref-excerpt">${description}</p>
  540. </div><strong class="ref-price">$${price}</strong>
  541. </div>
  542. <div id="addCartDiv_${_id}" class="ref-addons"></div>
  543. </div>
  544. `;
  545. htmlEl.appendChild(outerDiv);
  546. let addCartDiv = document.getElementById(`addCartDiv_${_id}`);
  547. addToCartBtn(addCartDiv, good);
  548. }
  549. const fillCurrentGoodContent = (good, htmlEl) => {
  550. htmlEl.innerHTML = '';
  551. const { name, _id, price, description, images } = good;
  552. htmlEl.innerHTML = `<h1>${name}</h1>
  553. <section>Description: ${description}</section>
  554. <section>Price: ${price}</section>
  555. `;
  556. htmlEl.innerHTML += `<section>Images:</section><br>` //вставить css display block
  557. for (const image of images) {
  558. htmlEl.innerHTML += `<img width="170px" src="${"http://shop-roles.node.ed.asmer.org.ua/"}${image.url}"</img><br>`//вставить css display block
  559. }
  560. addToCartBtn(htmlEl, good);
  561. }
  562. store.subscribe(() =>
  563. subscribePromiseItem(
  564. "goodFindOne", main, ["goods"], fillCurrentGoodContent));
  565. const subscribePromiseItem = (promiseName, htmlEl, subscrNames, execFunc) => {
  566. const [, route, _id] = location.hash.split('/');
  567. if ((subscrNames.length > 0 && (!route || !subscrNames.some(v => v == route)))/* || !_id*/)
  568. return;
  569. let reducerData = store.getState().promiseReducer[promiseName];
  570. if (!reducerData)
  571. return;
  572. const { status, payload, error } = reducerData;
  573. if (status === 'PENDING') {
  574. htmlEl.innerHTML = `<img src='https://cdn.dribbble.com/users/63485/screenshots/1309731/infinite-gif-preloader.gif' />`
  575. }
  576. else if (status == "FULFILLED") {
  577. execFunc(payload, htmlEl);
  578. }
  579. else if (status == "FULFILLED") {
  580. addErrorAlert(error.message);
  581. }
  582. }
  583. const getFullImageUrl = (image) =>
  584. `http://shop-roles.node.ed.asmer.org.ua/${image?.url}`;
  585. const showCartContent = (cart, htmlEl) => {
  586. htmlEl.innerHTML = '';
  587. htmlEl.innerHTML = `<h1>Cart</h1>
  588. `;
  589. htmlEl.innerHTML += '';//`<section>Items:</section><br>` //вставить css display block
  590. let allCount = 0;
  591. let htmlContent = '';
  592. for (const item of Object.values(cart)) {
  593. let { count, good } = item;
  594. let inpId = `inp_${good._id}`;
  595. let delBtnId = `delBtn_${good._id}`;
  596. htmlContent += `
  597. <div>
  598. <img width="170px" src="${getFullImageUrl(good.images[0])}"</img>
  599. <a href="#/goods/${good._id}">${good.name}</a>
  600. <input type="number" min="1" max="999" id="${inpId}" value="${count}">
  601. <button class="ref-button preview-toggle" id="${delBtnId}">Remove</button>
  602. </div>
  603. <br>`//вставить css display block
  604. allCount += count;
  605. }
  606. htmlContent += `<div>Count ${allCount}</div><br>`;
  607. htmlContent += `<button class="ref-button preview-toggle" id="btnCheckout">Checkout</button><br>`;
  608. htmlEl.innerHTML += htmlContent;
  609. for (const item of Object.values(cart)) {
  610. let { good } = item;
  611. let inpId = `inp_${good._id}`;
  612. let delBtnId = `delBtn_${good._id}`;
  613. let inp = document.getElementById(inpId);
  614. inp.addEventListener("change", function (e) { store.dispatch(actionCartSet(good, +inp.value)); });
  615. let delBtn = document.getElementById(delBtnId);
  616. delBtn.addEventListener("click", function (e) { store.dispatch(actionCartDel(good)); });
  617. }
  618. let btnCheckout = document.getElementById("btnCheckout");
  619. btnCheckout.addEventListener("click", function (e) {
  620. window.location = "#/checkout/";
  621. });
  622. }
  623. store.subscribe(() =>
  624. subscribeSimple(
  625. "cartReducer", main, ["cart"], showCartContent));
  626. const showOrder = (order, num, htmlEl) => {
  627. let htmlContent = `<div class="order d-none"><h2>Order: #${num}</h2>
  628. <!--<div>Created on: ${order.createdAt}</div>-->
  629. `;
  630. htmlContent += `<h3>Items:</h3>` //вставить css display block
  631. let orderGoods = Object.values(order.orderGoods);
  632. for (let i = 0; i < orderGoods.length; i++) {
  633. let { order, count, price, total, good } = orderGoods[i];
  634. htmlContent +=
  635. `
  636. <div class="ref-product align-items-center">
  637. <strong class="ref-count">${i}. </strong>
  638. <div class="ref-media"><img class="ref-image" width="170px" src="${getFullImageUrl(good.images[0])}"</img></div>
  639. <div class="ref-product-data">
  640. <div class="ref-product-info">
  641. <h5 class="ref-name"><a href="#/goods/${good._id}">${good.name}</a></h5>
  642. </div>
  643. <strong class="ref-price">Price: ${price}</strong>
  644. <strong class="ref-count">Count: ${count}</strong>
  645. <strong class="ref-price">Total: ${total}</strong>
  646. </div>
  647. </div>
  648. `//вставить css display block
  649. }
  650. htmlContent += `<h3 class="ref-count">Total ${order.total}</h3><hr/></div>`;
  651. htmlEl.innerHTML += htmlContent;
  652. }
  653. const showOrders = (orders, htmlEl) => {
  654. htmlEl.innerHTML = "<header>Orders:</header>";
  655. if (!localStorage?.authToken)
  656. return;
  657. if (orders) {
  658. let ordersContainerDiv = document.createElement("div");
  659. ordersContainerDiv.classList.add("container", "reflow-product-list");
  660. let ordersDiv = document.createElement("div");
  661. ordersContainerDiv.appendChild(ordersDiv);
  662. ordersDiv.classList.add("ref-products");
  663. for (let i = orders.length; i > 0; i--) {
  664. let order = orders[i - 1];
  665. showOrder(order, i, ordersDiv);
  666. }
  667. let paginationDiv = document.createElement("div");
  668. htmlEl.append(ordersContainerDiv, paginationDiv);
  669. new Pagination(ordersDiv, paginationDiv, 5, listItemTag = 'div.order')
  670. }
  671. }
  672. store.subscribe(() =>
  673. subscribePromiseItem(
  674. "orders", main, ["orders"], showOrders));
  675. const subscribeSimple = (reducerName, htmlEl, subscrNames, execFunc) => {
  676. const [, route, _id] = location.hash.split('/');
  677. if (!subscrNames || !subscrNames.some(v => v == route))
  678. return;
  679. let reducerData = store.getState()[reducerName];
  680. execFunc(reducerData, htmlEl);
  681. }
  682. window.onhashchange = () => {
  683. const [, route, _id] = location.hash.split('/')
  684. const routes = {
  685. categories() {
  686. console.log('Category', _id)
  687. store.dispatch(actionCategoryFindOne(_id))
  688. },
  689. subCategories() {
  690. console.log('subCategory', _id)
  691. store.dispatch(actionCategoryFindOne(_id))
  692. },
  693. goods() {
  694. console.log('good', _id)
  695. store.dispatch(actionGoodFindOne(_id))
  696. },
  697. cart() {
  698. console.log('cart')
  699. store.dispatch(actionCartShow())
  700. },
  701. checkout() {
  702. console.log('checkout');
  703. let state = store.getState();
  704. if (routes.login())
  705. store.dispatch(orderFullUpsert(() => window.location = "#/orders/"));
  706. },
  707. orders() {
  708. if (!localStorage.authToken) {
  709. showOrders([], main);
  710. return;
  711. }
  712. console.log('order');
  713. store.dispatch(actionFindOrders());
  714. },
  715. login() {
  716. if (!localStorage.authToken) {
  717. main.innerText = '';
  718. const form = new LoginForm(main);
  719. form.onLogin = (login, password) => {
  720. store.dispatch(actionFullLogin(login, password));
  721. }
  722. return false;
  723. }
  724. else if (route == "login") {
  725. window.location = "#/";
  726. window.location.reload();
  727. }
  728. return true;
  729. },
  730. register() {
  731. if (!localStorage.authToken) {
  732. main.innerText = '';
  733. const form = new LoginForm(main);
  734. form.onLogin = (login, password) => {
  735. store.dispatch(actionFullAuthUpsert(login, password));
  736. }
  737. return false;
  738. }
  739. else if (route == "register") {
  740. window.location = "#/";
  741. window.location.reload();
  742. }
  743. return true;
  744. },
  745. logout() {
  746. if (localStorage.authToken) {
  747. store.dispatch(actionAuthLogout());
  748. window.location = "#/login/";
  749. window.location.reload();
  750. }
  751. },
  752. //register(){
  753. ////нарисовать форму регистрации, которая по нажатию кнопки Login делает store.dispatch(actionFullRegister(login, password))
  754. //},
  755. }
  756. if (localStorage.authToken) {
  757. loginLink.classList.add('d-none');
  758. regLink.classList.add('d-none');
  759. historyLink.classList.remove('d-none');
  760. logoutLink.classList.remove('d-none');
  761. }
  762. else {
  763. loginLink.classList.remove('d-none');
  764. regLink.classList.remove('d-none');
  765. historyLink.classList.add('d-none');
  766. logoutLink.classList.add('d-none');
  767. }
  768. if (route in routes) {
  769. routes[route]()
  770. }
  771. }
  772. window.onhashchange()
  773. store.dispatch(actionRootCats());
  774. /*store.dispatch(actionCategoryFindOne("6262ca7dbf8b206433f5b3d1"));
  775. store.dispatch(actionGoodFindOne("62d3099ab74e1f5f2ec1a125"));
  776. store.dispatch(actionFullLogin("Berg", "123456789"));
  777. //store.dispatch(actionFullAuthUpsert("Berg1", "12345678911"));
  778. store.dispatch(actionCartAdd({ _id: '62d30938b74e1f5f2ec1a124', price: 50 }));
  779. delay(3000, () => {
  780. store.dispatch(orderFullUpsert());
  781. store.dispatch(actionFindOrders());
  782. });*/
  783. //delay(500, () => store.dispatch(actionFindOrders()));
  784. </script>
  785. </body>