script.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. // createstore
  2. function createStore(reducer) {
  3. let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
  4. let cbs = [] //массив подписчиков
  5. const getState = () => state //функция, возвращающая переменную из замыкания
  6. const subscribe = cb => (cbs.push(cb), //запоминаем подписчиков в массиве
  7. () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
  8. const dispatch = action => {
  9. if (typeof action === 'function') { //если action - не объект, а функция
  10. return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
  11. }
  12. const newState = reducer(state, action) //пробуем запустить редьюсер
  13. if (newState !== state) { //проверяем, смог ли редьюсер обработать action
  14. state = newState //если смог, то обновляем state
  15. for (let cb of cbs) cb() //и запускаем подписчиков
  16. }
  17. }
  18. return {
  19. getState,
  20. dispatch,
  21. subscribe
  22. }
  23. }
  24. // ----------------------------------------------------------
  25. // decode token
  26. function jwtDecode(token) {
  27. try {
  28. return JSON.parse(atob(token.split('.')[1]))
  29. }
  30. catch (e) {
  31. }
  32. }
  33. // ----------------------------------------------------------
  34. // reducers
  35. function authReducer(state = {}, { type, token }) { //authorise reducer
  36. if (state === undefined) {
  37. if (localStorage.authToken) {
  38. type = "AUTH_LOGIN";
  39. token = localStorage.authToken;
  40. }
  41. }
  42. if (type === 'AUTH_LOGIN') { //то мы логинимся
  43. const payload = jwtDecode(token)
  44. if (payload) {
  45. return {
  46. token,
  47. payload
  48. }
  49. }
  50. }
  51. if (type === 'AUTH_LOGOUT') { //мы разлогиниваемся
  52. return {}
  53. }
  54. return state
  55. }
  56. function promiseReducer(state = {}, { type, name, status, payload, error }) { //promise reducer
  57. if (type === 'PROMISE') {
  58. return {
  59. ...state,
  60. [name]: { status, payload, error }
  61. }
  62. }
  63. return state
  64. }
  65. function cartReducer(state = {}, { type, good, count = 1 }) {
  66. if (type === 'CART_ADD') {
  67. return {
  68. ...state,
  69. [good._id]: { count: (state[good._id]?.count || 0) + count, good: good }
  70. }
  71. }
  72. if (type === 'CART_CHANGE') {
  73. return {
  74. ...state,
  75. [good._id]: { count, good }
  76. }
  77. }
  78. if (type === 'CART_DELETE') {
  79. delete state[good._id]
  80. return {
  81. ...state,
  82. }
  83. }
  84. if (type === 'CART_CLEAR') {
  85. return {}
  86. }
  87. return state
  88. }
  89. const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count })
  90. const actionCartChange = (good, count = 1) => ({ type: 'CART_CHANGE', good, count })
  91. const actionCartDelete = (good) => ({ type: 'CART_DELETE', good })
  92. const actionCartClear = () => ({ type: 'CART_CLEAR' })
  93. function combineReducers(reducers) {
  94. function combinedReducer(combinedState = {}, action) {
  95. const newCombinedState = {}
  96. for (const [reducerName, reducer] of Object.entries(reducers)) {
  97. const newSubState = reducer(combinedState[reducerName], action)
  98. if (newSubState !== combinedState[reducerName]) {
  99. newCombinedState[reducerName] = newSubState
  100. }
  101. }
  102. if (Object.keys(newCombinedState).length === 0) {
  103. return combinedState
  104. }
  105. return { ...combinedState, ...newCombinedState }
  106. }
  107. return combinedReducer
  108. }
  109. const store = createStore(combineReducers({ promise: promiseReducer, auth: authReducer, cart: cartReducer }))
  110. // store.subscribe(() => console.log(store.getState()))
  111. // ----------------------------------------------------------
  112. // GQL
  113. const getGQL = url =>
  114. (query, variables) => fetch(url, {
  115. method: 'POST',
  116. headers: {
  117. "Content-Type": "application/json",
  118. ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } : {})
  119. },
  120. body: JSON.stringify({ query, variables })
  121. }).then(res => res.json())
  122. .then(data => {
  123. if (data.data) {
  124. return Object.values(data.data)[0]
  125. }
  126. else throw new Error(JSON.stringify(data.errors))
  127. })
  128. const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/'
  129. const gql = getGQL(backendURL + 'graphql')
  130. // ----------------------------------------------------------
  131. // promises
  132. const actionPromise = (name, promise) =>
  133. async dispatch => {
  134. dispatch(actionPending(name))
  135. try {
  136. let payload = await promise
  137. dispatch(actionFulfilled(name, payload))
  138. return payload
  139. }
  140. catch (e) {
  141. dispatch(actionRejected(name, e))
  142. }
  143. }
  144. const actionPending = (name) => ({ type: 'PROMISE', status: 'PENDING', name })
  145. const actionFulfilled = (name, payload) => ({ type: 'PROMISE', status: 'FULFILLED', name, payload })
  146. const actionRejected = (name, error) => ({ type: 'PROMISE', status: 'REJECTED', name, error })
  147. // ----------------------------------------------------------
  148. // actions
  149. const actionFullRegister = (login, password) =>
  150. actionPromise('fullRegister', gql(`mutation UserUpsert($login: String, $password: String){UserUpsert(user: {login:$login,password:$password}){_id}}`, { login: login, password: password }))
  151. const actionAuthLogin = (token) =>
  152. (dispatch, getState) => {
  153. const oldState = getState()
  154. dispatch({ type: 'AUTH_LOGIN', token })
  155. const newState = getState()
  156. if (oldState !== newState)
  157. localStorage.authToken = token
  158. }
  159. const actionAuthLogout = () =>
  160. dispatch => {
  161. dispatch({ type: 'AUTH_LOGOUT' })
  162. localStorage.removeItem('authToken')
  163. }
  164. const actionFullLogin = (login, password) =>
  165. actionPromise('fullLogin', gql(`query login($login:String,$password:String){login(login:$login,password:$password)}`, { login: login, password: password }))
  166. const orderFind = () =>
  167. actionPromise('orderFind', gql(`query orderFind{
  168. OrderFind(query: "[{}]"){
  169. _id createdAt total orderGoods {_id price count good{name price images{url}}}
  170. }
  171. }`, { q: JSON.stringify([{}]) }))
  172. const actionAddOrder = (cart) =>
  173. actionPromise('actionAddOrder', gql(`mutation newOrder($cart: [OrderGoodInput])
  174. {OrderUpsert(order: {orderGoods: $cart})
  175. {_id total}}`, { cart: cart }))
  176. const actionRootCats = () =>
  177. actionPromise('rootCats', gql(`query {
  178. CategoryFind(query: "[{\\"parent\\":null}]"){
  179. _id name
  180. }
  181. }`))
  182. const actionCatById = (_id) => // √
  183. actionPromise('catById', gql(`query catById($q: String){
  184. CategoryFindOne(query: $q){
  185. _id name goods {
  186. _id name price images {
  187. url
  188. }
  189. }
  190. subCategories{_id name}
  191. }
  192. }`, { q: JSON.stringify([{ _id }]) }))
  193. const actionGoodById = (_id) =>
  194. actionPromise('goodById', gql(`query goodById($q: String){
  195. GoodFindOne(query: $q){
  196. _id name price description images {
  197. url
  198. }
  199. }
  200. }`, { q: JSON.stringify([{ _id }]) }))
  201. store.dispatch(actionRootCats())
  202. // ----------------------------------------------------------
  203. // subscribe
  204. store.subscribe(() => {
  205. const { rootCats } = (store.getState()).promise
  206. if (rootCats?.payload) {
  207. cat.innerHTML = `<li class="list-group-item"><b>Categories</b></li>`
  208. for (const { _id, name } of rootCats?.payload) {
  209. const categories = document.createElement('li')
  210. categories.innerHTML = `<a href='#/category/${_id}'>${name}</a>`
  211. categories.style = ' padding-left: 30px ; '
  212. cat.append(categories)
  213. }
  214. }
  215. else if (!rootCats) {
  216. cat.innerHTML = '<img src = img/preloader.gif>'
  217. }
  218. })
  219. store.subscribe(() => {
  220. const { catById } = (store.getState()).promise
  221. const [, route, _id] = location.hash.split('/')
  222. if (catById?.payload && route === 'category') {
  223. main.innerHTML = ``
  224. const { name } = catById.payload
  225. const card = document.createElement('div')
  226. card.id = 'sub-card'
  227. card.innerHTML = `<h2 id="cat-name"><b>${name}</b></h2><br>`
  228. const backPart = document.createElement('div')
  229. backPart.id = 'back'
  230. const backBtn = document.createElement('button')
  231. backBtn.setAttribute('type', 'button');
  232. backBtn.addEventListener('click', () => {
  233. history.back();
  234. });
  235. backBtn.innerText = '⬅'
  236. backPart.appendChild(backBtn)
  237. if (catById.payload.subCategories) {
  238. for (const { _id, name } of catById.payload?.subCategories) {
  239. card.innerHTML += `<a href='#/category/${_id}' class='subcategories'>${name}</a>`
  240. }
  241. }
  242. // card.append(backPart)
  243. main.append(card, backPart)
  244. for (const { _id, name, price, images } of catById.payload?.goods) {
  245. const card = document.createElement('div')
  246. card.id = 'card'
  247. card.innerHTML = `<h5><b>${name}</b></h5>
  248. <div class='card-img'><img src="http://shop-roles.node.ed.asmer.org.ua/${images[0].url}"/></div><br>
  249. <h2 style='color: green; text-align:center'>Price: $${price}</h2><br><br>
  250. <a class="" style="width: 100%;" href='#/good/${_id}'>More details -> </a><br><br>`
  251. let button = document.createElement('button')
  252. button.innerText = 'BUY'
  253. button.className = 'buy-btn'
  254. button.setAttribute('type', 'button');
  255. button.onclick = async () => {
  256. await store.dispatch(actionCartAdd({ _id: _id, name: name, price: price, images: images }))
  257. console.log('hi')
  258. }
  259. card.append(button)
  260. main.append(card)
  261. }
  262. }
  263. })
  264. store.subscribe(() => {
  265. const { goodById } = (store.getState()).promise
  266. const [, route, _id] = location.hash.split('/')
  267. if (goodById?.payload && route === 'good') {
  268. const { _id, name, description, images, price } = goodById.payload
  269. main.innerHTML = `<h1>${name}</h1>`
  270. const card = document.createElement('div')
  271. card.id = 'desc-card'
  272. const backPart = document.createElement('div')
  273. backPart.id = 'back'
  274. const backBtn = document.createElement('button')
  275. backBtn.setAttribute('type', 'button');
  276. backBtn.addEventListener('click', () => {
  277. history.back();
  278. });
  279. backBtn.innerText = '⬅'
  280. backPart.appendChild(backBtn)
  281. let block = document.createElement('div')
  282. block.id = 'price'
  283. block.innerHTML = `<h2>Price: <b class='price'>$${price}</b></h2>`
  284. let button = document.createElement('button')
  285. button.innerText = 'BUY'
  286. button.className = 'buy-btn'
  287. button.setAttribute('type', 'button');
  288. button.style = 'height:80px'
  289. button.onclick = async () => {
  290. await store.dispatch(actionCartAdd({ _id: _id, name: name, price: price, images: images }))
  291. console.log('hi')
  292. }
  293. card.innerHTML = `<img src="http://shop-roles.node.ed.asmer.org.ua/${images[0].url}" /><br><br>`
  294. card.append(block)
  295. card.innerHTML += `<p><b>Description:</b> ${description}</p>`
  296. main.append(backPart, card, button)
  297. }
  298. })
  299. store.subscribe(() => {
  300. const { orderFind } = (store.getState()).promise
  301. const [, route, _id] = location.hash.split('/')
  302. if (orderFind?.payload && route === 'orderFind') {
  303. main.innerHTML = '<h1>ORDER HISTORY</h1>'
  304. for (const { _id, createdAt, total, orderGoods } of orderFind.payload.reverse()) {
  305. const card = document.createElement('div')
  306. card.className = 'order-card'
  307. card.innerHTML = `<h3>Order: ${createdAt}</h3>`
  308. for (const { count, good } of orderGoods) {
  309. const divGood = document.createElement('div')
  310. divGood.style = "display:flex;margin-bottom: 20px;"
  311. divGood.innerHTML += `<div><b>${good.name}</b><br> Price: <b>$${good.price}</b><br> Amount: <b>${count} pt</b></b></div><img style="max-width: 80px;margin-right: 20px;display: block;margin-left: auto;" src="http://shop-roles.node.ed.asmer.org.ua/${good.images[0].url}"/><br><br>`
  312. card.append(divGood)
  313. }
  314. card.innerHTML += 'Date: <b>' + new Date(+createdAt).toLocaleString().replace(/\//g, '.') + '</b>'
  315. card.innerHTML += `<br><h2>Total: <b style="color:green;">$${total}</b></h2>`
  316. main.append(card)
  317. }
  318. }
  319. })
  320. // ----------------------------------------------------------
  321. // window
  322. function display() {
  323. let token = localStorage.authToken
  324. if (token) {
  325. form_yes.style.display = 'flex'
  326. form_no.style.display = 'none'
  327. UserNick.innerText = JSON.parse(window.atob(localStorage.authToken.split('.')[1])).sub.login
  328. } else {
  329. form_yes.style.display = 'none'
  330. form_no.style.display = 'flex'
  331. }
  332. }
  333. display()
  334. window.onhashchange = () => {
  335. const [, route, _id] = location.hash.split('/')
  336. const routes = {
  337. category() {
  338. store.dispatch(actionCatById(_id))
  339. },
  340. good() {
  341. store.dispatch(actionGoodById(_id))
  342. },
  343. login() {
  344. main.innerHTML = ''
  345. let form = document.createElement('div')
  346. let div1 = document.createElement('div')
  347. let div2 = document.createElement('div')
  348. div1.innerHTML = `<h1>LOGIN</h1>`
  349. let loginInput = document.createElement('input')
  350. loginInput.placeholder = 'Type your username'
  351. loginInput.name = 'login'
  352. div1.style.display = 'flex'
  353. div1.style.flexDirection = 'column'
  354. let passwordInput = document.createElement('input')
  355. passwordInput.placeholder = 'Type your password'
  356. passwordInput.name = 'password'
  357. passwordInput.type = 'password';
  358. div1.append(loginInput)
  359. div2.append(passwordInput)
  360. let button = document.createElement('button')
  361. button.innerText = "LOGIN"
  362. button.id = 'login-btn'
  363. button.setAttribute('type', 'button');
  364. button.onclick = async () => {
  365. let tokenPromise = async () => await store.dispatch(actionFullLogin(loginInput.value, passwordInput.value))
  366. let token = await tokenPromise()
  367. if (token !== null) {
  368. store.dispatch(actionAuthLogin(token))
  369. display()
  370. document.location.href = "#/orderFind";
  371. }
  372. else {
  373. loginInput.value = ''
  374. passwordInput.value = ''
  375. alert("Incorrect username or password.")
  376. store.dispatch(actionAuthLogout())
  377. }
  378. }
  379. form.append(div1, div2, button)
  380. main.append(form)
  381. },
  382. register() {
  383. main.innerHTML = ''
  384. let form = document.createElement('div')
  385. let div1 = document.createElement('div')
  386. let div2 = document.createElement('div')
  387. div1.innerHTML += `<h1>REGISTER</h1>`
  388. let loginInput = document.createElement('input')
  389. loginInput.placeholder = "Type your username"
  390. div1.append(loginInput)
  391. let passwordInput = document.createElement('input')
  392. passwordInput.placeholder = "Type your password"
  393. passwordInput.type = 'password'
  394. div2.append(passwordInput)
  395. let button = document.createElement('button')
  396. button.innerText = "CREATE ACCOUNT"
  397. button.id = 'reg-btn'
  398. button.setAttribute('type', 'button');
  399. let textAlert = document.createElement('div')
  400. let textAlert2 = document.createElement('div')
  401. let putInText = "Username and password required!"
  402. let userAlready = "An account with this username already exist!"
  403. textAlert.append(userAlready)
  404. textAlert2.append(putInText)
  405. textAlert2.style = 'display : none; color : red'
  406. textAlert.style = 'display : none; color : red'
  407. button.onclick = async () => {
  408. let register = await store.dispatch(actionFullRegister(loginInput.value, passwordInput.value))
  409. let tokenPromise = async () => await store.dispatch(actionFullLogin(loginInput.value, passwordInput.value))
  410. if (loginInput.value == '' || passwordInput.value == '') {
  411. textAlert2.style.display = 'block'
  412. } else {
  413. if (register !== null) {
  414. let token = await tokenPromise()
  415. store.dispatch(actionAuthLogin(token))
  416. // console.log(token)
  417. display()
  418. document.location.href = "#/orderFind";
  419. } else {
  420. textAlert.style.display = 'block'
  421. textAlert2.style.display = 'none'
  422. }
  423. }
  424. }
  425. form.append(div1, div2, button)
  426. form.append(textAlert, textAlert2)
  427. main.append(form)
  428. },
  429. orderFind() {
  430. store.dispatch(orderFind())
  431. },
  432. cart() {
  433. main.innerHTML = '<h1>CART</h1>'
  434. for (const [_id, obj] of Object.entries(store.getState().cart)) {
  435. const card = document.createElement('div')
  436. card.style = 'width: 33.33%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px;display: flex; flex-direction: column ; align-items: center ; justify-content: space-between'
  437. const { count, good } = obj
  438. card.innerHTML += `Products: <b>${good.name}</b> <br><img src="http://shop-roles.node.ed.asmer.org.ua/${good.images[0].url}" style="width: 100px"/> <br> Цена: <b>$${good.price}</b><br><br>`
  439. const calculation = document.createElement('div')
  440. const buttonAdd = document.createElement('button')
  441. buttonAdd.innerHTML = '+'
  442. buttonAdd.setAttribute('type', 'button');
  443. buttonAdd.onclick = async () => {
  444. inputCount.value = +inputCount.value + 1
  445. await store.dispatch(actionCartChange({ _id: _id, name: good.name, price: good.price, images: good.images }, +inputCount.value))
  446. cardTotal.innerHTML = `<br><h2>Total: <b style="color:green;">$${goodPrice()}</b></h2><br>`
  447. }
  448. calculation.append(buttonAdd)
  449. const inputCount = document.createElement('input')
  450. inputCount.value = +count
  451. inputCount.disabled = 'disabled'
  452. inputCount.className = 'inputCount'
  453. calculation.append(inputCount)
  454. const buttonLess = document.createElement('button')
  455. buttonLess.innerHTML = '-'
  456. buttonLess.setAttribute('type', 'button');
  457. buttonLess.onclick = async () => {
  458. if ((+inputCount.value) > 1) {
  459. inputCount.value = +inputCount.value - 1
  460. await store.dispatch(actionCartChange({ _id: _id, name: good.name, price: good.price, images: good.images }, +inputCount.value))
  461. cardTotal.innerHTML = `<br><h2>Total: <b style="color:green;">$${goodPrice()}</b></h2><br>`
  462. }
  463. }
  464. calculation.append(buttonLess)
  465. const buttonDelete = document.createElement('button')
  466. buttonDelete.innerText = 'Delete'
  467. buttonDelete.className = 'buttonDelete'
  468. buttonDelete.setAttribute('type', 'button');
  469. buttonDelete.onclick = async () => {
  470. await store.dispatch(actionCartDelete({ _id: _id, name: good.name, price: good.price, images: good.images }))
  471. card.style.display = 'none'
  472. cardTotal.innerHTML = `<br><h2>Total: <b style="color:green;">$${goodPrice()}</b></h2><br>`
  473. }
  474. card.append(calculation)
  475. card.append(buttonDelete)
  476. main.append(card)
  477. }
  478. const cardTotalDiv = document.createElement('div')
  479. cardTotalDiv.id = 'total'
  480. const cardTotal = document.createElement('div')
  481. cardTotal.innerHTML = `<br><h2>Total: <b style="color:green;">$${goodPrice()}</b></h2>`
  482. cardTotalDiv.append(cardTotal)
  483. let cartAlert = document.createElement('div')
  484. cartAlert.innerHTML = `<h2 style='color:orange;'>Your cart seems empty 😟</h2>`
  485. cartAlert.style.display = 'none'
  486. cartAlert.id = 'cart-alert'
  487. if (localStorage.authToken != '') {
  488. const button = document.createElement('button')
  489. button.innerHTML += '<strong>ORDER</strong>'
  490. button.setAttribute('type', 'button');
  491. if(goodPrice() != 0){
  492. button.onclick = async () => {
  493. await store.dispatch(actionAddOrder(Object.entries(store.getState().cart).map(([_id, count]) => ({ count: count.count, good: { _id } }))));
  494. await store.dispatch(actionCartClear());
  495. document.location.href = "#/orderFind";
  496. }
  497. }
  498. else{
  499. cartAlert.style.display = 'flex'
  500. }
  501. // button.className = 'btn btn-primary'
  502. cardTotalDiv.append(button)
  503. }
  504. main.append(cardTotalDiv, cartAlert)
  505. }
  506. }
  507. if (route in routes) {
  508. routes[route]()
  509. }
  510. }
  511. window.onhashchange()
  512. function localStoredReducer(reducer, localStorageKey) {
  513. function wrapperReducer(state, action) {
  514. if (state === undefined) { //если загрузка сайта
  515. try {
  516. return JSON.parse(localStorage[localStorageKey]) //пытаемся распарсить сохраненный
  517. //в localStorage state и подсунуть его вместо результата редьюсера
  518. }
  519. catch (e) { } //если распарсить не выйдет, то код пойдет как обычно:
  520. }
  521. const newState = reducer(state, action)
  522. localStorage.setItem(localStorageKey, JSON.stringify(newState)) //сохраняем состояние в localStorage
  523. return newState
  524. }
  525. return wrapperReducer
  526. }
  527. window.onhashchange()
  528. function goodPrice() {
  529. return Object.entries(store.getState().cart).map(i => x += i[1].count * i[1].good.price, x = 0).reverse()[0] || 0
  530. }