magic.js 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004
  1. // url of backend
  2. const url = 'http://shop-roles.node.ed.asmer.org.ua/graphql'
  3. // Вспомогательные функции ========================================================================================================
  4. // getGql - переделка из HW18 функции gql (делаем запрос на бэк)
  5. function getGql(endpoint) {
  6. return async function gql(query, variables = {}) {
  7. let headers = {
  8. 'Content-Type': 'application/json;charset=utf-8',
  9. 'Accept': 'application/json',
  10. }
  11. if (('authToken' in localStorage)) {
  12. headers.Authorization = 'Bearer ' + localStorage.authToken
  13. }
  14. let result = await fetch(endpoint, {
  15. method: 'POST',
  16. headers,
  17. body: JSON.stringify({
  18. query,
  19. variables
  20. })
  21. }).then(res => res.json())
  22. if (('errors' in result) && !('data' in result)) {
  23. throw new Error(JSON.stringify(result.errors))
  24. }
  25. result = Object.values(result.data)[0]
  26. return result
  27. }
  28. }
  29. const gql = getGql(url)
  30. //=========================================================================================================
  31. // localStoredReducer
  32. function localStoredReducer(originalReducer, localStorageKey) {
  33. function wrapper(state, action) {
  34. if (!state) {
  35. try {
  36. return JSON.parse(localStorage[localStorageKey])
  37. }
  38. catch (error) {
  39. }
  40. }
  41. const newState = originalReducer(state, action)
  42. localStorage[localStorageKey] = JSON.stringify(newState)
  43. return newState
  44. }
  45. return wrapper
  46. }
  47. // Redux ========================================================================================================
  48. // createStore
  49. function createStore(reducer) {
  50. let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
  51. let cbs = [] //массив подписчиков
  52. const getState = () => state //функция, возвращающая переменную из замыкания
  53. const subscribe = cb => (cbs.push(cb), //запоминаем подписчиков в массиве
  54. () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
  55. const dispatch = action => {
  56. if (typeof action === 'function') { //если action - не объект, а функция
  57. return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
  58. }
  59. const newState = reducer(state, action) //пробуем запустить редьюсер
  60. if (newState !== state) { //проверяем, смог ли редьюсер обработать action
  61. state = newState //если смог, то обновляем state
  62. for (let cb of cbs) {
  63. cb()
  64. } //и запускаем подписчиков
  65. }
  66. }
  67. return {
  68. getState,
  69. dispatch,
  70. subscribe
  71. }
  72. }
  73. // создание combineReducers
  74. function combineReducers(reducers) {
  75. function totalReducer(state = {}, action) {
  76. const newTotalState = {}
  77. for (const [reducerName, reducer] of Object.entries(reducers)) {
  78. const newSubState = reducer(state[reducerName], action)
  79. if (newSubState !== state[reducerName]) {
  80. newTotalState[reducerName] = newSubState
  81. }
  82. }
  83. if (Object.keys(newTotalState).length) {
  84. return { ...state, ...newTotalState }
  85. }
  86. return state
  87. }
  88. return totalReducer
  89. }
  90. // создание promiseReducer
  91. function promiseReducer(state = {}, { type, status, payload, error, nameOfPromise }) {
  92. if (type === 'PROMISE') {
  93. return {
  94. ...state,
  95. [nameOfPromise]: { status, payload, error }
  96. }
  97. }
  98. return state
  99. }
  100. // акшоны для promiseReducer
  101. const actionPending = nameOfPromise => ({ nameOfPromise, type: 'PROMISE', status: 'PENDING' })
  102. const actionFulfilled = (nameOfPromise, payload) => ({ nameOfPromise, type: 'PROMISE', status: 'FULFILLED', payload })
  103. const actionRejected = (nameOfPromise, error) => ({ nameOfPromise, type: 'PROMISE', status: 'REJECTED', error })
  104. const actionPromise = (nameOfPromise, promise) =>
  105. async dispatch => {
  106. dispatch(actionPending(nameOfPromise)) //сигнализируем redux, что промис начался
  107. try {
  108. const payload = await promise //ожидаем промиса
  109. dispatch(actionFulfilled(nameOfPromise, payload)) //сигнализируем redux, что промис успешно выполнен
  110. return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса
  111. }
  112. catch (error) {
  113. dispatch(actionRejected(nameOfPromise, error)) //в случае ошибки - сигнализируем redux, что промис несложился
  114. }
  115. }
  116. // authReducer
  117. // раскодируем JWT-токен
  118. const jwtDecode = function (token) {
  119. try {
  120. let parseData = token.split('.')[1]
  121. return JSON.parse(atob(parseData))
  122. }
  123. catch (e) {
  124. return undefined
  125. }
  126. }
  127. function authReducer(state = {}, { type, token }) {
  128. if (type === 'AUTH_LOGIN') {
  129. let payload = jwtDecode(token)
  130. return state = {
  131. token,
  132. payload
  133. }
  134. }
  135. if (type === 'AUTH_LOGOUT') {
  136. localStorage.removeItem('authToken')
  137. return {}
  138. }
  139. return state
  140. }
  141. // акшон для логинизации
  142. const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token })
  143. // акшон для раззлогинивания
  144. const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' })
  145. // cartReducer
  146. function cartReducer(state = {}, { type, count, good }) {
  147. if (type === 'CART_ADD') {
  148. return {
  149. ...state,
  150. [good._id]: {
  151. good,
  152. count: (state[good._id] ? state[good._id].count + count : count)
  153. }
  154. }
  155. }
  156. if (type === 'CART_SUB') {
  157. if (state[good._id]) {
  158. let newCount = state[good._id].count - count
  159. if (newCount > 0) {
  160. return {
  161. ...state,
  162. [good._id]: {
  163. good,
  164. count: newCount
  165. }
  166. }
  167. } else {
  168. delete state[good._id]
  169. return { ...state }
  170. }
  171. } else {
  172. return undefined
  173. }
  174. }
  175. if (type === 'CART_DEL') {
  176. delete state[good._id]
  177. return { ...state }
  178. }
  179. if (type === 'CART_SET') {
  180. if (count > 0) {
  181. return {
  182. ...state,
  183. [good._id]: {
  184. good,
  185. count
  186. }
  187. }
  188. } else {
  189. delete state[good._id]
  190. return { ...state }
  191. }
  192. }
  193. if (type === 'CART_CLEAR') {
  194. return {}
  195. }
  196. return state
  197. }
  198. // акшоны для cartReducer
  199. // Добавление товара.Должен добавлять новый ключ в state, или обновлять, если ключа в state ранее не было, увеличивая количество
  200. const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count })
  201. // Уменьшение количества товара.Должен уменьшать количество товара в state, или удалять его если количество будет 0 или отрицательным
  202. const actionCartSub = (good, count = 1) => ({ type: 'CART_SUB', count, good })
  203. // Удаление товара.Должен удалять ключ из state
  204. const actionCartDel = (good) => ({ type: 'CART_DEL', good })
  205. // Задание количества товара.В отличие от добавления и уменьшения, не учитывает того количества, которое уже было в корзине, а тупо назначает количество поверху(или создает новый ключ, если в корзине товара не было).Если count 0 или отрицательное число - удаляем ключ из корзины;
  206. const actionCartSet = (good, count = 1) => ({ type: 'CART_SET', count, good })
  207. // Очистка корзины.state должен стать пустым объектом { }
  208. const actionCartClear = () => ({ type: 'CART_CLEAR' })
  209. // объект со всеми редьюсерами
  210. const reducers = {
  211. promise: localStoredReducer(promiseReducer, 'promise'),
  212. auth: localStoredReducer(authReducer, 'auth'),
  213. cart: localStoredReducer(cartReducer, 'cart'),
  214. }
  215. const totalReducer = combineReducers(reducers)
  216. const store = createStore(totalReducer)
  217. store.subscribe(() => console.log(store.getState())) // для контроля выводим все изменения в магазине в консоль
  218. // GraphQL запросы переписанные сразу в акшоны ========================================================================================================
  219. // Запрос на список корневых категорий (без родителей)
  220. const actionCategoryFind = () => actionPromise('CategoryFind', gql(`query baseCategory($searchVariablesCategory: String){
  221. CategoryFind(query: $searchVariablesCategory){
  222. _id name parent {
  223. _id
  224. name
  225. }
  226. }
  227. }`, {
  228. searchVariablesCategory: JSON.stringify([{ parent: null }])
  229. }))
  230. store.dispatch(actionCategoryFind()) // и сразу же все запустили, чтоб постоянно отрисовывалась
  231. // Запрос для получения одной категории с товарами и картинками
  232. const actionCategoryFindOne = _id => actionPromise('CategoryFindOne', gql(`query categoryFindOne($searchVariablesCategoryOne: String,) {
  233. CategoryFindOne(query: $searchVariablesCategoryOne){
  234. _id name
  235. goods{
  236. _id name description price
  237. images{
  238. url
  239. }
  240. }
  241. subCategories{
  242. _id name
  243. }
  244. }
  245. }`, {
  246. searchVariablesCategoryOne: JSON.stringify([{ _id }])
  247. }))
  248. // Запрос на получение товара с описанием и картинками
  249. const actionGoodFindOne = _id => actionPromise('GoodFindOne', gql(`query oneGoodWithImages($searchVariablesGoodOne: String) {
  250. GoodFindOne(query: $searchVariablesGoodOne){
  251. _id name price description images {
  252. url
  253. }
  254. }
  255. }`, {
  256. searchVariablesGoodOne: JSON.stringify([{ _id }])
  257. }))
  258. // Запрос на логин
  259. const actionLogin = (login, password) => actionPromise('login', gql(`query login($login: String, $password: String) {
  260. login(login: $login, password: $password)
  261. }`, {
  262. login,
  263. password
  264. }))
  265. // показываем ошибку при авторизации, если неправильный логин или пароль
  266. function mistakeLogin() {
  267. main.innerHTML = `<p style="color: red;">Вы ввели неправильный логин или пароль</p>
  268. <button id='refreshBtn'>Повторить попытку</button>`
  269. const refresh = document.getElementById('refreshBtn')
  270. refresh.addEventListener('click', () => {
  271. location.reload()
  272. })
  273. }
  274. // Запрос на логин и последующую логинизацию в authReduser (thunk)
  275. const actionFullLogin = (login, password) =>
  276. async dispatch => {
  277. const token = await dispatch(actionLogin(login, password))
  278. if (token != null) {
  279. if (typeof (token) === 'string') {
  280. dispatch(actionAuthLogin(token))
  281. localStorage.authToken = token
  282. }
  283. } else {
  284. mistakeLogin()
  285. }
  286. }
  287. // Запрос на регистрацию
  288. const actionUserCreate = (login, password) => actionPromise('UserRegistrate', gql(`mutation registration($login:String,$password:String ){
  289. UserUpsert(user:{
  290. login:$login, password:$password
  291. }){
  292. _id createdAt
  293. }
  294. }`, {
  295. login,
  296. password
  297. }))
  298. // показываем ошибку при регистрации, если логин занят
  299. function mistakeRegistration() {
  300. main.innerHTML = `<p style="color: red;">Этот логин занят. Повторите регистрацию с уникальным логином или <a href="#/login/">авторизуйтесь</a></p>
  301. <button id='refreshBtn'>Повторить попытку</button>`
  302. const refresh = document.getElementById('refreshBtn')
  303. refresh.addEventListener('click', () => {
  304. location.reload()
  305. })
  306. }
  307. // Запрос на регистрацию и сразу на авторизацию пользователя на странице (thunk)
  308. const actionFullUserCreate = (login, password) =>
  309. async dispatch => {
  310. try {
  311. const registration = await dispatch(actionUserCreate(login, password))
  312. if (registration._id !== 'null') {
  313. dispatch(actionFullLogin(login, password))
  314. }
  315. }
  316. catch (e) {
  317. mistakeRegistration()
  318. }
  319. }
  320. // Запрос истории заказов
  321. const actionOrderFind = () => actionPromise('OrderFind', gql(`query order($order: String){
  322. OrderFind(query: $order){
  323. createdAt total orderGoods{
  324. good {
  325. name price images {
  326. url
  327. }
  328. }
  329. total count
  330. }
  331. }
  332. }`, {
  333. order: JSON.stringify([{}])
  334. }))
  335. // запрос отправку заказа на сервер
  336. const actionOrder = (goods) => actionPromise('orderCreate', gql(`mutation myOrder($createOrder: OrderInput){
  337. OrderUpsert(order: $createOrder) {
  338. orderGoods{
  339. count good{
  340. _id
  341. }
  342. }
  343. }
  344. }`, {
  345. createOrder: { orderGoods: goods }
  346. }))
  347. // запрос отправку заказа на сервер с последующей очисткой корзины (thunk)
  348. const actionFillOrder = () =>
  349. async dispatch => {
  350. // создаем массив с параметрами для диспатча
  351. let arrWithGoods = []
  352. for (const [id, { count }] of Object.entries(store.getState().cart)) {
  353. arrWithGoods.push({ count: count, good: { _id: id } })
  354. }
  355. // оформляем заказ
  356. await dispatch(actionOrder(arrWithGoods))
  357. // и как задиспатчится заказ чистим корзину
  358. store.dispatch(actionCartClear())
  359. }
  360. // Модуль =========================================================================================================
  361. // отрисовка в main списка товаров из категории
  362. const drawGoods = () => {
  363. const [, route] = location.hash.split('/')
  364. if (route !== 'category') return
  365. const { status, payload, error } = store.getState().promise.CategoryFindOne
  366. if (status === 'PENDING') {
  367. main.innerHTML = `<img src='https://cdn.dribbble.com/users/63485/screenshots/1309731/infinite-gif-preloader.gif'/>`
  368. }
  369. if (status === 'FULFILLED') {
  370. const { _id, name, goods, subCategories } = payload // подумать, нужны ли ид и субкатегория (субкатегория может ыть нужна, чтобы отрисовать ее в )
  371. main.innerHTML = `<h1>Категория: ${name}</h1>`
  372. // рисуем блок для субкатегорий
  373. const drawSubCat = document.createElement('div')
  374. drawSubCat.id = 'drawSubCat'
  375. main.append(drawSubCat)
  376. // отрисовываем карточки категорий
  377. // тут проблема. в айфонах subCategories из-за этого вот так с проверкой
  378. if (subCategories != null) {
  379. for (const { _id, name } of subCategories) {
  380. drawSubCat.innerHTML += `<div class="goodListCart" id='drawSubCatBtn'>
  381. <h2>${name}</h2>
  382. <a href="#/category/${_id}">Подробнее</a>
  383. </div>`
  384. }
  385. }
  386. // рисуем блок для товаров в категории
  387. const goodsList = document.createElement('div')
  388. goodsList.id = 'goodList'
  389. main.append(goodsList)
  390. // отрисовываем карточку товара в разделе категорий
  391. if (goods != null) {
  392. for (const { name, description, images, price, _id } of goods) { // удалить description из запроса и из отрисовки
  393. let img = url.slice(0, -7) + images[0].url
  394. goodsList.innerHTML += `
  395. <div class="goodListCart">
  396. <img src ="${img}" >
  397. <h2>${name}</h2>
  398. <p>Цена: ${price} грн.</p>
  399. <a href="#/good/${_id}">Подробнее</a>
  400. </div>`
  401. }
  402. }
  403. }
  404. }
  405. store.subscribe(drawGoods)
  406. // отрисовка товара после нажатия на карточку товара
  407. const drawGoodOne = () => {
  408. const [, route] = location.hash.split('/')
  409. if (route !== 'good') return
  410. const { status, payload, error } = store.getState().promise.GoodFindOne
  411. if (status === 'PENDING') {
  412. main.innerHTML = `<img src='https://cdn.dribbble.com/users/63485/screenshots/1309731/infinite-gif-preloader.gif'/>`
  413. }
  414. if (status === 'FULFILLED') {
  415. const { _id, name, description, price, images } = payload
  416. let img = url.slice(0, -7) + images[0].url
  417. main.innerHTML = `<img src="${img}">
  418. <h1>${name}</h1>
  419. <h2>Цена: ${price} грн./шт.</h2>
  420. <h3>Описание:</h3>
  421. <p>${description}</p>
  422. <div id=''>
  423. <label for="lname">Количество товара</label><br>
  424. <input type="number" min="0" value="0" id="addToCartInput" name="lname"><br><br>
  425. <input id="addToCartBtn" type="submit" value="Добавить в корзину" disabled="true">
  426. </div>`
  427. // включаем кнопку "добавить товар", только если указали количество товара
  428. addToCartInput.addEventListener('change', () => {
  429. if (addToCartInput.value == 0) {
  430. addToCartBtn.disabled = true
  431. } else {
  432. addToCartBtn.disabled = false
  433. }
  434. // addToCartBtn.disabled = (addToCartInput.value == 0 || false) // или можно написать вот так эту проверку
  435. })
  436. // Добавляем товар в корзину по клику кнопки "добавить"
  437. addToCartBtn.addEventListener('click', () => {
  438. store.dispatch(actionCartAdd({ _id, name, description, price, img }, +addToCartInput.value))
  439. })
  440. }
  441. }
  442. store.subscribe(drawGoodOne)
  443. //отрисовка истории заказов пользователя
  444. const history = () => {
  445. const [, route] = location.hash.split('/')
  446. if (route !== 'history') return
  447. const { status, payload, error } = store.getState().promise.OrderFind
  448. if (status === 'PENDING') {
  449. main.innerHTML = `<img src='https://cdn.dribbble.com/users/63485/screenshots/1309731/infinite-gif-preloader.gif'/>`
  450. }
  451. if (status === 'FULFILLED') {
  452. main.innerHTML = `<h1>История заказов</h1>`
  453. for (const { createdAt, total, orderGoods } of payload) {
  454. const orderFindCart = document.createElement('div')
  455. orderFindCart.classList = 'orderFindCart'
  456. main.append(orderFindCart)
  457. // формируем дату создания поста
  458. const dateOfOrder = new Date(+createdAt)
  459. const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']
  460. const dateOfOrderParse = `${dateOfOrder.getDate() < 10 ? '0' + dateOfOrder.getDate() : dateOfOrder.getDate()} ${months[dateOfOrder.getMonth()]} ${dateOfOrder.getFullYear()} ${dateOfOrder.getHours()}:${dateOfOrder.getMinutes() < 10 ? '0' + dateOfOrder.getMinutes() : dateOfOrder.getMinutes()}`
  461. orderFindCart.innerHTML = `
  462. <p>Создано: ${dateOfOrderParse}</p>
  463. <h2>Сумма заказа: ${total} тугриков</h2>`
  464. for (const { good: { name, price, images }, count, total } of orderGoods) {
  465. let img = url.slice(0, -7) + images[0].url
  466. orderFindCart.innerHTML += `<div class="orderFindCart">
  467. <img src ="${img}">
  468. <div class="orderFindCartDescription">
  469. <p>Товар: ${name}</p>
  470. <p>Цена товара: ${price}</p>
  471. <p>Количество товара в заказе: ${count}</p>
  472. <p>Итоговая сумма: ${total}</p>
  473. </div>
  474. </div>`
  475. }
  476. }
  477. }
  478. }
  479. store.subscribe(history)
  480. // отрисовываем левое меню
  481. const leftMenu = () => {
  482. const { status, payload, error } = store.getState().promise.CategoryFind
  483. if (status === 'FULFILLED' && payload) {
  484. aside.innerHTML = ''
  485. aside.innerHTML = `<a href="/">Главная</a>`
  486. for (const { _id, name } of payload) {
  487. aside.innerHTML += `<a href="#/category/${_id}">${name}</a>`
  488. }
  489. }
  490. }
  491. store.subscribe(leftMenu)
  492. // изменение цифры количества товаров в корзине (в красном лейбле)
  493. const redLabel = () => {
  494. let goodCount = 0
  495. for (let { count } of Object.values(store.getState().cart)) {
  496. goodCount += count
  497. }
  498. document.getElementById('cartIcon').textContent = goodCount
  499. }
  500. store.subscribe(redLabel)
  501. // подписчик для отображения залогинен или нет пользователь, а также для отображения страниц регистрации и логинизации в шапке
  502. const userStatus = () => {
  503. if (!(store.getState().auth.payload?.sub?.login)) {
  504. login.innerHTML = `<a href ="#/login/">Авторизоваться</a></br>
  505. <a href ="#/register/">Зарегистрироваться</a></br>`
  506. } else {
  507. login.innerHTML = `Привет, ${store.getState().auth.payload?.sub?.login}</br>
  508. <a href="#/history/">История заказов</a></br>
  509. <a href="" id="logout">Выйти</a>`
  510. logout.onclick = () => {
  511. store.dispatch(actionAuthLogout())
  512. }
  513. }
  514. }
  515. store.subscribe(userStatus)
  516. // функция для страницы корзины. из-за тго, что очень массивная страница с большим функционалом, отрисовывааем ее отдельно
  517. function cartPage() {
  518. const [, route] = location.hash.split('/')
  519. if (route !== 'cart') return
  520. if (Object.keys(store.getState().cart).length !== 0) {
  521. main.innerHTML = '<h1>Корзина</h1>'
  522. // подсчитываем сумму всего заказа (изначально при входе в корзину)
  523. let summ = 0
  524. for (const { count, good: { price } } of Object.values(store.getState().cart)) {
  525. summ += (count * price)
  526. }
  527. // создаем с помощью цикла карточки товаров в заказе
  528. for (const [id, { count, good: { description, img, name, price, _id } }] of Object.entries(store.getState().cart)) {
  529. const goodInCart = document.createElement('div')
  530. goodInCart.id = "goodInCart"
  531. goodInCart.innerHTML = `<h2>${name}</h2>`
  532. main.append(goodInCart)
  533. const goodInCartContent = document.createElement('div')
  534. goodInCartContent.id = 'goodInCartContent'
  535. goodInCart.append(goodInCartContent)
  536. const goodInCartIimg = document.createElement('div')
  537. goodInCartIimg.id = 'goodInCartIimg'
  538. goodInCartIimg.innerHTML = `<img src="${img}">`
  539. goodInCartContent.append(goodInCartIimg)
  540. const goodInCartIDescription = document.createElement('div')
  541. goodInCartIDescription.id = 'goodInCartIDescription'
  542. goodInCartContent.append(goodInCartIDescription)
  543. // рисуем кнопки на карточке товара
  544. //кнопка удаления
  545. const delBtn = document.createElement('button')
  546. delBtn.innerHTML = '-'
  547. goodInCartIDescription.append(delBtn)
  548. const goodInCartValue = document.createElement('input')
  549. goodInCartValue.id = 'goodInCartValue'
  550. goodInCartValue.value = count
  551. goodInCartIDescription.append(goodInCartValue)
  552. // кнопка добавления
  553. const addBtn = document.createElement('button')
  554. addBtn.innerHTML = '+'
  555. goodInCartIDescription.append(addBtn)
  556. // создаем блок с кнопками управления
  557. const blockWithButtons = document.createElement('div')
  558. goodInCartIDescription.append(blockWithButtons)
  559. //кнопка универсального добавления товара
  560. const cartSetBtn = document.createElement('button')
  561. cartSetBtn.innerHTML = 'Добавить товар'
  562. blockWithButtons.append(cartSetBtn)
  563. // кнопка удаления товара из корзины
  564. const cartDelBtn = document.createElement('button')
  565. cartDelBtn.innerHTML = 'Удалить товар'
  566. blockWithButtons.append(cartDelBtn)
  567. const goodInCartAbout = document.createElement('p')
  568. goodInCartAbout.innerHTML = `Описание: ${description}.</br></br>Цена за ед. ${price} тугриков.`
  569. goodInCartIDescription.append(goodInCartAbout)
  570. const goodInCartFullPrice = document.createElement('p')
  571. goodInCartFullPrice.innerHTML = `Итоговая стоимость за товар: ${count * price} тугриков`
  572. // goodInCartFullPrice.innerHTML = `Итоговая стоимость за товар: ${store.getState().cart[_id].count * price} тугриков`
  573. goodInCartIDescription.append(goodInCartFullPrice)
  574. // блок колбеков на кнопках
  575. // коллбек на кнопку +
  576. addBtn.addEventListener('click', async () => {
  577. await store.dispatch(actionCartAdd({ _id, name, description, price, img }))
  578. const newCount = store.getState().cart[_id].count
  579. goodInCartValue.value = newCount
  580. // изменяем стоимость на карточке товара
  581. goodInCartFullPrice.innerHTML = `Итоговая стоимость за товар: ${newCount * price} тугриков`
  582. })
  583. // коллбек на кнопку -
  584. delBtn.addEventListener('click', async () => {
  585. await store.dispatch(actionCartSub({ _id, name, description, price, img }))
  586. // прверяем, если после этого товара не осталось (удалили последний товар), перерисовываем страницу. если товар еще есть в заказе, просто понимажем его счетчик
  587. if (store.getState().cart[_id]) {
  588. const newCount = store.getState().cart[_id].count
  589. goodInCartValue.value = newCount
  590. // изменяем стоимость карточки
  591. goodInCartFullPrice.innerHTML = `Итоговая стоимость за товар: ${newCount * price} тугриков`
  592. } else {
  593. cartPage()
  594. }
  595. })
  596. // коллбек на кнопку "Добавить товар"
  597. cartSetBtn.addEventListener('click', async () => {
  598. const newCount = goodInCartValue.value
  599. // проверяем число на 0 или больше
  600. if (newCount > 0) {
  601. await store.dispatch(actionCartSet({ _id, name, description, price, img }, +newCount))
  602. // изменяем стоимость карточки
  603. goodInCartFullPrice.innerHTML = `Итоговая стоимость за товар: ${newCount * price} тугриков`
  604. // изменяем общую сумму заказа
  605. // cartOrderFullPrice.innerHTML = `Сумма заказа: ${summ} тугриков`
  606. } else {
  607. await store.dispatch(actionCartDel({ _id }))
  608. // и сразу обновляем страницу корзины
  609. cartPage()
  610. }
  611. })
  612. // коллбек на кнопку "Удалить товар"
  613. cartDelBtn.addEventListener('click', async () => {
  614. await store.dispatch(actionCartDel({ _id }))
  615. // и сразу обновляем страницу корзины
  616. cartPage()
  617. })
  618. }
  619. // отрисовываем блок под карточками товаров с общей стоимостью покупки и кнопками "Оформить заказ" и "Очистить корзину"
  620. const cartOrderFullPrice = document.createElement('p')
  621. cartOrderFullPrice.innerHTML = `Общая сумма заказа: ${summ} тугриков`
  622. main.append(cartOrderFullPrice)
  623. const cartOrderCreate = document.createElement('button')
  624. cartOrderCreate.id = 'cartOrderCreateBtn'
  625. cartOrderCreate.innerHTML = 'Оформить заказ'
  626. main.append(cartOrderCreate)
  627. const breakLine = document.createElement('br')
  628. main.append(breakLine)
  629. const cartClean = document.createElement('button')
  630. cartClean.innerHTML = 'Очистить корзину'
  631. main.append(cartClean)
  632. // коллбек на кнопку очистки корзины
  633. cartClean.addEventListener('click', async () => {
  634. await store.dispatch(actionCartClear())
  635. // и сразу обновляем страницу корзины
  636. cartPage()
  637. })
  638. // коллбек на кнопку оформления заказа
  639. cartOrderCreate.addEventListener('click', async () => {
  640. await store.dispatch(actionFillOrder())
  641. // и сразу обновляем страницу корзины
  642. cartPage()
  643. })
  644. // подписчик для изменения отображения на странице корзины
  645. const summFinally = () => {
  646. // просчитываем полную сумму заказа
  647. let summ = 0
  648. for (const { count, good: { price } } of Object.values(store.getState().cart)) {
  649. summ += (count * price)
  650. }
  651. // меняем общую стоимость заказа при изменении стора
  652. cartOrderFullPrice.innerHTML = `Сумма заказа: ${summ} тугриков`
  653. }
  654. store.subscribe(summFinally)
  655. } else {
  656. main.innerHTML = `<p>Ваша корзина пуста.Чтобы сделать заказ, сначала добавьте товары.</p>`
  657. }
  658. }
  659. // функция - конструктор формы логин/пароль =========================================================================================================
  660. function LoginForm(parent) {
  661. function Password(parent, open) {
  662. // отображение формы для пароля
  663. const inputPassword = document.createElement('input')
  664. inputPassword.type = 'password'
  665. inputPassword.placeholder = 'Insert password'
  666. parent.append(inputPassword)
  667. // создание и отображение чекбокса (открыть/скрыть пароль)
  668. const inputCheckbox = document.createElement('input')
  669. inputCheckbox.type = 'checkbox'
  670. inputCheckbox.checked = false
  671. parent.append(inputCheckbox)
  672. // создаем геттеры
  673. this.getValue = () => inputPassword.value
  674. this.getOpen = () => inputCheckbox.checked
  675. // создаем сеттеры
  676. this.setValue = (value) => inputPassword.value = value
  677. this.setOpen = (open) => {
  678. if (open === true) {
  679. inputPassword.type = 'text'
  680. inputCheckbox.checked = true
  681. }
  682. if (open === false) {
  683. inputPassword.type = 'password'
  684. inputCheckbox.checked = false
  685. }
  686. return inputPassword.type, inputCheckbox.checked
  687. }
  688. // starting onChange
  689. inputPassword.addEventListener('input', () => {
  690. this.onChange(inputPassword.value)
  691. })
  692. // starting onOpenChange + change inputPasswor hide/see
  693. inputCheckbox.addEventListener('change', () => {
  694. this.setOpen(inputCheckbox.checked)
  695. this.onOpenChange(inputCheckbox.checked)
  696. })
  697. }
  698. function Login(parent) {
  699. // создаем и отрысовываем поле для ввода логина
  700. const inputLogin = document.createElement('input')
  701. inputLogin.type = 'text'
  702. inputLogin.placeholder = 'Insert login'
  703. parent.append(inputLogin)
  704. // создаем геттеры
  705. this.getValue = () => inputLogin.value
  706. // создаем сеттеры
  707. this.setValue = (value) => inputLogin.value = value
  708. // starting onChange
  709. inputLogin.addEventListener('input', () => {
  710. this.onChange(inputLogin.value)
  711. })
  712. }
  713. // создание и отрисовывание формы для логина/пароля
  714. // const form = document.createElement('form') // если формируем форму, а не обычный див, тогда не работает запрос и появляется в адресной строке "?" после домена в урле
  715. const form = document.createElement('div')
  716. parent.append(form)
  717. const loginLabel = document.createElement('label')
  718. loginLabel.innerText = 'Login:'
  719. form.append(loginLabel)
  720. let breakSymbol = document.createElement('br')
  721. form.append(breakSymbol)
  722. // отрисовываем поле логина
  723. const login = new Login(form)
  724. breakSymbol = document.createElement('br')
  725. form.append(breakSymbol)
  726. const passwordLabel = document.createElement('label')
  727. passwordLabel.innerText = 'Password:'
  728. form.append(passwordLabel)
  729. breakSymbol = document.createElement('br')
  730. form.append(breakSymbol)
  731. // отрисовываем поле для пароля
  732. const password = new Password(form, true)
  733. breakSymbol = document.createElement('br')
  734. form.append(breakSymbol)
  735. // создание и отрисовывание кнопки для входа
  736. const confirmBtn = document.createElement('button')
  737. confirmBtn.innerText = 'Confirm'
  738. confirmBtn.id = 'confirmBtn'
  739. confirmBtn.type = 'submit'
  740. confirmBtn.style.marginTop = '10px'
  741. confirmBtn.disabled = true
  742. form.append(confirmBtn)
  743. // change confirmBtn.disabled
  744. function checkButton() {
  745. if (login.getValue() !== '' && password.getValue() !== '') {
  746. confirmBtn.disabled = false
  747. } else {
  748. confirmBtn.disabled = true
  749. }
  750. return confirmBtn.disabled
  751. }
  752. checkButton()
  753. // listening password and login
  754. login.onChange = password.onChange = checkButton
  755. // create getters
  756. this.getPasswordValue = () => password.getValue()
  757. this.getPasswordOpen = () => password.getOpen()
  758. this.getLoginValue = () => login.getValue()
  759. this.getButtonStatus = () => confirmBtn.disabled
  760. // create setters
  761. this.setPasswordValue = (value) => password.setValue(value)
  762. this.setLoginValue = (value) => login.setValue(value)
  763. this.setPasswordOpen = (status) => password.setOpen(status)
  764. // create callbacks
  765. this.onOpenChange = open => password.onOpenChange = open
  766. }
  767. // проверяем, что сейчас находится в урле и, исходя из приставки в урле, запускаем нужную функцию
  768. window.onhashchange = () => {
  769. const [, route, _id] = location.hash.split('/')
  770. const routes = {
  771. category() {
  772. store.dispatch(actionCategoryFindOne(_id))
  773. },
  774. good() {
  775. store.dispatch(actionGoodFindOne(_id))
  776. },
  777. login() {
  778. if (!localStorage.authToken) { // если пользователь залогинен, то ему больше не рисовать форму логина
  779. main.innerHTML = '<h2>Авторизуйтесь</h2>'
  780. //нарисовать форму логина, которая по нажатию кнопки Login делает store.dispatch(actionFullLogin(login, password))
  781. const logForm = new LoginForm(main)
  782. confirmBtn.addEventListener('click', async () => {
  783. localStorage.removeItem('authToken') // на всякий случай (а нужно ли?) удаляю старый authToken перед регистрацией
  784. await store.dispatch(actionFullLogin(logForm.getLoginValue(), logForm.getPasswordValue()))
  785. // if (store.getState().promise.login.payload != null) {
  786. if ((Object.keys(store.getState().auth)).length) {
  787. main.innerHTML = `<h2>С возвращением, ${logForm.getLoginValue()}!<h2>`
  788. }
  789. })
  790. }
  791. },
  792. register() {
  793. if (!localStorage.authToken) { // если пользователь залогинен, то ему больше не отображать страницу регистрации (не рисовать форму)
  794. main.innerHTML = `<h2>Зарегистрируйтесь</h2>`
  795. // Запрос на регистрацию. Страница # / register /.В onhashchange НЕ происходит dispatch, вместо этого рисуется форма логина из предыдущих ДЗ, по логину же происходит dispatch
  796. const regForm = new LoginForm(main)
  797. confirmBtn.addEventListener('click', async () => {
  798. localStorage.removeItem('authToken') // на всякий случай (а нужно ли?) удаляю старый authToken перед регистрацией
  799. await store.dispatch(actionFullUserCreate(regForm.getLoginValue(), regForm.getPasswordValue()))
  800. if ((Object.keys(store.getState().auth)).length) {
  801. main.innerHTML = `<h2>Поздравляем, регистраций прошла успешно!<h2>`
  802. }
  803. })
  804. }
  805. },
  806. history() {
  807. store.dispatch(actionOrderFind())
  808. },
  809. cart() {
  810. if (localStorage.authToken) { // если пользователь авторизован - показываем корзину, если нет - предлагаем авторизоваться!
  811. cartPage()
  812. } else {
  813. main.innerHTML = `<h1> Корзина</h1>
  814. <p>Для просмотра корзины Вам необходимо авторизоваться!</p>
  815. <a href="#/login/">Авторизация</a>`
  816. }
  817. }
  818. }
  819. if (route in routes) {
  820. routes[route]()
  821. }
  822. }
  823. window.onhashchange()
  824. // 3. разобраться с тем, почему корзина хуйней страдает
  825. // 6. сonst form = document.createElement('form') // если формируем форму, а не обычный див на логине, тогда не работает запрос и появляется в адресной строке "?" после домена в урле