index2.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. // !store
  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, //добавление функции getState в результирующий объект
  20. dispatch,
  21. subscribe //добавление subscribe в объект
  22. }
  23. }
  24. // ! decoding token
  25. function jwtDecode(token){
  26. try {
  27. return JSON.parse(atob(token.split('.')[1]))
  28. }
  29. catch(e){
  30. }
  31. }
  32. // ! reducers
  33. function authReducer(state, {type, token}){
  34. if(state === undefined){
  35. if(localStorage.authToken){
  36. token = localStorage.authToken
  37. type = 'AUTH_LOGIN'
  38. }
  39. else{
  40. type = 'AUTH_LOGOUT'
  41. }
  42. }
  43. if(type === 'AUTH_LOGIN'){
  44. state = jwtDecode(token)
  45. localStorage.authToken = token
  46. }
  47. if(type === 'AUTH_LOGOUT'){
  48. localStorage.authToken = ''
  49. state = {}
  50. }
  51. return state || {}
  52. }
  53. function promiseReducer(state={}, {type, name, status, payload, error}){
  54. if (type === 'PROMISE'){
  55. return {
  56. ...state,
  57. [name]:{status, payload, error}
  58. }
  59. }
  60. return state
  61. }
  62. function cartReducer(state={}, {type, good, count=1}){
  63. if (type === 'CART_ADD'){
  64. return {
  65. ...state,
  66. [good._id]: {count: (state[good._id]?.count || 0) + count, good:good}
  67. }
  68. }
  69. if (type === 'CART_CHANGE'){
  70. return {
  71. ...state,
  72. [good._id]: {count, good}
  73. }
  74. }
  75. if (type === 'CART_DELETE'){
  76. delete state[good._id]
  77. return {
  78. ...state,
  79. }
  80. }
  81. if (type === 'CART_CLEAR'){
  82. return {}
  83. }
  84. return state
  85. }
  86. const actionCartAdd = (good, count=1) => ({type: 'CART_ADD', good, count})
  87. const actionCartChange = (good, count=1) => ({type: 'CART_CHANGE', good, count})
  88. const actionCartDelete = (good) => ({type: 'CART_DELETE', good})
  89. const actionCartClear = () => ({type: 'CART_CLEAR'})
  90. function combineReducers(reducers){ //пачку редьюсеров как объект {auth: authReducer, promise: promiseReducer}
  91. function combinedReducer(combinedState={}, action){ //combinedState - типа {auth: {...}, promise: {....}}
  92. const newCombinedState = {}
  93. for (const [reducerName, reducer] of Object.entries(reducers)){
  94. const newSubState = reducer(combinedState[reducerName], action)
  95. if (newSubState !== combinedState[reducerName]){
  96. newCombinedState[reducerName] = newSubState
  97. }
  98. }
  99. if (Object.keys(newCombinedState).length === 0){
  100. return combinedState
  101. }
  102. return {...combinedState, ...newCombinedState}
  103. }
  104. return combinedReducer //нам возвращают один редьюсер, который имеет стейт вида {auth: {...стейт authReducer-а}, promise: {...стейт promiseReducer-а}}
  105. }
  106. const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart:cartReducer}))
  107. store.subscribe(() => console.log(store.getState()))
  108. // ! GQL
  109. const getGQL = url =>
  110. (query, variables) => fetch(url, {
  111. method: 'POST',
  112. headers: {
  113. "Content-Type": "application/json",
  114. ...(localStorage.authToken ? {"Authorization": "Bearer " + localStorage.authToken} : {})
  115. },
  116. body: JSON.stringify({query, variables})
  117. }).then(res => res.json())
  118. .then(data => {
  119. if (data.data){
  120. return Object.values(data.data)[0]
  121. }
  122. else throw new Error(JSON.stringify(data.errors))
  123. })
  124. const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/graphql'
  125. const gql = getGQL(backendURL + '/graphql')
  126. // !PROMISE
  127. const actionPromise = (name, promise) =>
  128. async dispatch => {
  129. dispatch(actionPending(name))
  130. try {
  131. let payload = await promise
  132. dispatch(actionFulfilled(name, payload))
  133. return payload
  134. }
  135. catch(error){
  136. dispatch(actionRejected(name, error))
  137. }
  138. }
  139. const actionPending = name => ({type:'PROMISE',name, status: 'PENDING'})
  140. const actionFulfilled = (name,payload) => ({type:'PROMISE',name, status: 'FULFILLED', payload})
  141. const actionRejected = (name,error) => ({type:'PROMISE',name, status: 'REJECTED', error})
  142. // ! ACTIONS
  143. const actionFullRegister = (login, password) =>
  144. actionPromise('fullRegister', gql(`mutation UserUpsert($login: String, $password: String){UserUpsert(user: {login:$login,password:$password}){_id}}`, {login: login, password:password}))
  145. const actionAuthLogin = (token) =>
  146. (dispatch, getState) => {
  147. const oldState = getState()
  148. dispatch({type: 'AUTH_LOGIN', token})
  149. const newState = getState()
  150. if (oldState !== newState)
  151. localStorage.authToken = token
  152. }
  153. const actionFullLogin = (login, password) => //вход
  154. actionPromise('fullLogin', gql(`query login($login:String,$password:String){login(login:$login,password:$password)}`, {login: login, password:password}))
  155. const actionAuthLogout = () =>
  156. dispatch => {
  157. dispatch({type: 'AUTH_LOGOUT'})
  158. localStorage.removeItem('authToken')
  159. }
  160. const orderFind = () => //история заказов
  161. actionPromise('orderFind', gql(`query orderFind{
  162. OrderFind(query: "[{}]"){
  163. _id createdAt total orderGoods {_id price count good{name price images{url}}}
  164. }
  165. }`, {q: JSON.stringify([{}])}))
  166. const actionAddOrder = (cart) => //оформ. заказа
  167. actionPromise('actionAddOrder', gql(`mutation newOrder($cart: [OrderGoodInput])
  168. {OrderUpsert(order: {orderGoods: $cart})
  169. {_id total}}`, {cart: cart}))
  170. const actionRootCats = () =>
  171. actionPromise('rootCats', gql(`query {
  172. CategoryFind(query: "[{\\"parent\\":null}]"){
  173. _id name
  174. }
  175. }`))
  176. const actionCatById = (_id) =>
  177. actionPromise('catById', gql(`query catById($q: String){
  178. CategoryFindOne(query: $q){
  179. _id name goods {
  180. _id name price images {
  181. url
  182. }
  183. }
  184. subCategories{_id name}
  185. }
  186. }`, {q: JSON.stringify([{_id}])}))
  187. const actionGoodById = (_id) =>
  188. actionPromise('goodById', gql(`query goodById($q: String){
  189. GoodFindOne(query: $q){
  190. _id name price description images {
  191. url
  192. }
  193. }
  194. }`, {q: JSON.stringify([{_id}])}))
  195. store.dispatch(actionRootCats())
  196. // !SUBSCRIBE
  197. store.subscribe(() => {
  198. const {rootCats} = (store.getState()).promise
  199. if (rootCats?.payload){
  200. aside.innerHTML = `<li class="list-group-item"><b>Категории</b></li>`
  201. for (const {_id, name} of rootCats?.payload){
  202. const categories = document.createElement('li')
  203. categories.innerHTML = `<a href='#/category/${_id}'>${name}</a>`
  204. categories.style = ' padding-left: 30px ; '
  205. aside.append(categories)
  206. }
  207. }
  208. })
  209. store.subscribe(() => {
  210. const {catById} = (store.getState()).promise
  211. const [,route, _id] = location.hash.split('/')
  212. if (catById?.payload && route === 'category'){
  213. main.innerHTML = ``
  214. const {name} = catById.payload
  215. const card = document.createElement('div')
  216. card.style = 'height: auto;width: 100%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px;'
  217. card.innerHTML = `<h4><b>${name}</b></h4><br>`
  218. if(catById.payload.subCategories){
  219. for (const {_id, name} of catById.payload?.subCategories){
  220. card.innerHTML +=`<a href='#/category/${_id}' class='subcategories'>${name}</a>`
  221. }
  222. }
  223. main.append(card)
  224. for (const {_id, name, price, images} of catById.payload?.goods){
  225. const card = document.createElement('div')
  226. card.style = 'height: auto;width: 30%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px; display: flex ; flex-direction: column ; justify-content: space-between'
  227. card.innerHTML = `<h5><b>${name}</b></h5>
  228. <img src="http://shop-roles.node.ed.asmer.org.ua/${images[0].url}" style="max-width: 100%; max-height: 300px;"/><br>
  229. <strong>Цена: ${price} грн.</strong><br><br>
  230. <a class="" style="width: 100%;" href='#/good/${_id}'>Подробнее</a><br><br>`
  231. let button = document.createElement('button')
  232. button.innerText = 'Купить'
  233. button.className = 'btn-buy'
  234. button.style = 'width: 100%; font-family: Impact; letter-spacing : 1px'
  235. button.onclick = async () => {
  236. await store.dispatch(actionCartAdd({_id: _id, name: name, price: price, images: images}))
  237. console.log('tap')
  238. }
  239. card.append(button)
  240. main.append(card)
  241. }
  242. }
  243. })
  244. store.subscribe(() => {
  245. const {goodById} = (store.getState()).promise
  246. const [,route, _id] = location.hash.split('/')
  247. if (goodById?.payload && route === 'good'){
  248. const {name,description,images,price} = goodById.payload
  249. main.innerHTML = `<h1>${name}</h1>`
  250. const card = document.createElement('div')
  251. card.innerHTML = `<img src="http://shop-roles.node.ed.asmer.org.ua/${images[0].url}" /><br>
  252. <b>Цена: ${price} грн.</b><br>
  253. <p><b>Описание:</b> ${description}</p>`
  254. main.append(card)
  255. }
  256. })
  257. store.subscribe(() => {
  258. const {orderFind} = (store.getState()).promise
  259. const [,route, _id] = location.hash.split('/')
  260. if (orderFind?.payload && route === 'orderFind'){
  261. main.innerHTML='<h1>История заказов</h1>'
  262. for (const {_id, createdAt, total,orderGoods} of orderFind.payload.reverse()){
  263. const card = document.createElement('div')
  264. card.style = 'width: 100%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px;'
  265. card.innerHTML = `<h3>Заказ: ${createdAt}</h3>`
  266. for (const {count, good} of orderGoods){2
  267. const divGood = document.createElement('div')
  268. divGood.style= "display:flex;margin-bottom: 20px;"
  269. divGood.innerHTML += `<div>Товар: <b>${good.name}</b><br> Цена: <b>${good.price} грн.</b><br> Количество: <b>${count} шт.</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>`
  270. card.append(divGood)
  271. }
  272. card.innerHTML += 'Дата: <b>'+new Date(+createdAt).toLocaleString().replace(/\//g, '.')+'</b>'
  273. card.innerHTML += `<br>Всего: <b style="color:red;">${total} грн.</b>`
  274. main.append(card)
  275. }
  276. }
  277. })
  278. // !WINDOW
  279. function display(){
  280. let token = localStorage.authToken
  281. if(token){
  282. form_yes.style.display = 'block'
  283. form_no.style.display = 'none'
  284. UserNick.innerText=JSON.parse(window.atob(localStorage.authToken.split('.')[1])).sub.login
  285. }else{
  286. form_yes.style.display = 'none'
  287. form_no.style.display = 'block'
  288. }
  289. }
  290. display()
  291. window.onhashchange = () => {
  292. const [, route, _id] = location.hash.split('/')
  293. mainContainer.scrollTo(0,0);
  294. const routes = {
  295. category(){
  296. store.dispatch(actionCatById(_id))
  297. },
  298. good(){
  299. store.dispatch(actionGoodById(_id))
  300. },
  301. login(){
  302. main.innerHTML = ''
  303. let form = document.createElement('div')
  304. let div = document.createElement('div')
  305. div.innerHTML += `<h1>Вход</h1>`
  306. let inputLogin = document.createElement('input')
  307. inputLogin.placeholder="Login"
  308. inputLogin.name = "login"
  309. div.append(inputLogin)
  310. form.append(div)
  311. let div2 = document.createElement('div')
  312. div.style.display = 'flex'
  313. div.style.flexDirection = 'column'
  314. let inputPassword = document.createElement('input')
  315. inputPassword.placeholder = "Password"
  316. inputPassword.name = "password"
  317. div2.append(inputPassword)
  318. form.append(div2)
  319. let button = document.createElement('button')
  320. button.innerText="Войти"
  321. button.style.padding = '15px 35px'
  322. button.style.marginTop = '20px'
  323. button.style.backgroundColor = 'yellowgreen'
  324. button.style.textTransform = 'uppercase'
  325. button.style.fontFamily = 'Impact'
  326. button.style.fontSize = '15px'
  327. button.onclick = async () => {
  328. let tokenPromise = async () => await store.dispatch(actionFullLogin(inputLogin.value, inputPassword.value))
  329. let token = await tokenPromise()
  330. if(token!==null){
  331. store.dispatch(actionAuthLogin(token))
  332. console.log(token)
  333. display()
  334. document.location.href = "#/orderFind";
  335. }
  336. else{
  337. inputLogin.value = ''
  338. inputPassword.value = ''
  339. alert("Введен неверный логин или пароль !")
  340. store.dispatch(actionAuthLogout())
  341. }
  342. }
  343. form.append(button)
  344. main.append(form)
  345. },
  346. register(){
  347. main.innerHTML = ''
  348. let form = document.createElement('div')
  349. let div = document.createElement('div')
  350. div.innerHTML += `<h1>Регистрация</h1>`
  351. let inputLogin = document.createElement('input')
  352. inputLogin.placeholder="Login"
  353. div.append(inputLogin)
  354. form.append(div)
  355. let div2 = document.createElement('div')
  356. let inputPassword = document.createElement('input')
  357. inputPassword.placeholder="Password"
  358. div2.append(inputPassword)
  359. form.append(div2)
  360. let button = document.createElement('button')
  361. button.innerText="Зарегистрироваться"
  362. button.style.padding = '15px 35px'
  363. button.style.marginTop = '20px'
  364. button.style.backgroundColor = 'yellowgreen'
  365. button.style.textTransform = 'uppercase'
  366. button.style.fontFamily = 'Impact'
  367. button.style.fontSize = '15px'
  368. let textAlert = document.createElement('div')
  369. let textAlert2 = document.createElement('div')
  370. let putInText = "Введите данные!"
  371. let userAlready = "Пользователь с таким логином уже зарегистрирован!"
  372. textAlert.append(userAlready)
  373. textAlert2.append(putInText)
  374. textAlert2.style = 'display : none; color : red'
  375. textAlert.style = 'display : none; color : red'
  376. button.onclick = async () => {
  377. let register = await store.dispatch(actionFullRegister(inputLogin.value, inputPassword.value))
  378. let tokenPromise = async () => await store.dispatch(actionFullLogin(inputLogin.value, inputPassword.value))
  379. if(inputLogin.value == '' || inputPassword.value == ''){
  380. textAlert2.style.display = 'block'
  381. }else{
  382. if(register !==null){
  383. let token = await tokenPromise()
  384. store.dispatch(actionAuthLogin(token))
  385. console.log(token)
  386. display()
  387. document.location.href = "#/orderFind";
  388. }else{
  389. textAlert.style.display = 'block'
  390. textAlert2.style.display = 'none'
  391. }
  392. }
  393. }
  394. form.append(textAlert , textAlert2)
  395. form.append(button)
  396. main.append(form)
  397. },
  398. orderFind(){
  399. store.dispatch(orderFind())
  400. },
  401. car(){
  402. main.innerHTML = '<h1>Корзина</h1>'
  403. for (const [_id, obj] of Object.entries(store.getState().cart)){
  404. const card = document.createElement('div')
  405. 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'
  406. const {count, good} = obj
  407. card.innerHTML += `Товар: <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>`
  408. const calculation = document.createElement('div')
  409. const buttonAdd = document.createElement('button')
  410. buttonAdd.innerHTML = '+'
  411. buttonAdd.style.width = '35px'
  412. buttonAdd.onclick = async () => {
  413. inputCount.value = +inputCount.value + 1
  414. await store.dispatch(actionCartChange({_id: _id, name: good.name, price: good.price, images: good.images}, +inputCount.value))
  415. cardTotal.innerHTML = `<br>Всего: <b style="color:red;">${goodPrice()} грн.</b><br>`
  416. }
  417. calculation.append(buttonAdd)
  418. const inputCount = document.createElement('input')
  419. inputCount.value = +count
  420. inputCount.disabled = 'disabled'
  421. inputCount.className = 'inputCount'
  422. calculation.append(inputCount)
  423. const buttonLess = document.createElement('button')
  424. buttonLess.innerHTML = '-'
  425. buttonLess.style.width = '35px'
  426. buttonLess.onclick = async () => {
  427. if((+inputCount.value)>1){
  428. inputCount.value = +inputCount.value - 1
  429. await store.dispatch(actionCartChange({_id: _id, name: good.name, price: good.price, images: good.images}, +inputCount.value))
  430. cardTotal.innerHTML = `<br>Всего: <b style="color:red;">${goodPrice()} грн.</b><br>`
  431. }
  432. }
  433. calculation.append(buttonLess)
  434. const buttonDelete = document.createElement('button')
  435. buttonDelete.innerText = 'Удалить'
  436. buttonDelete.className = 'buttonDelete'
  437. buttonDelete.onclick = async () => {
  438. await store.dispatch(actionCartDelete({_id: _id, name: good.name, price: good.price, images: good.images}))
  439. card.style.display = 'none'
  440. cardTotal.innerHTML = `<br>Всего: <b style="color:red;">${goodPrice()} грн.</b><br>`
  441. }
  442. card.append(calculation)
  443. card.append(buttonDelete)
  444. main.append(card)
  445. }
  446. const cardTotalDiv = document.createElement('div')
  447. const cardTotal = document.createElement('div')
  448. cardTotalDiv.style = 'position : absolute; display : flex;right : 50px; bottom: 0px'
  449. cardTotal.innerHTML = `<br>Всего: <b style="color:red;">${goodPrice()} грн.</b>`
  450. cardTotalDiv.append(cardTotal)
  451. if(localStorage.authToken!=''){
  452. const button = document.createElement('button')
  453. button.innerHTML += 'ЗАКАЗАТЬ'
  454. button.style = ' background-color : yellowgreen; font-family: Impact; font-size : 40px'
  455. button.onclick = async () => {
  456. await store.dispatch(actionAddOrder(Object.entries(store.getState().cart).map(([_id, count]) => ({count:count.count,good:{_id}}))));
  457. await store.dispatch(actionCartClear());
  458. document.location.href = "#/orderFind";
  459. }
  460. button.className = 'btn btn-primary'
  461. cardTotalDiv.append(button)
  462. }
  463. main.append(cardTotalDiv)
  464. }
  465. }
  466. if (route in routes)
  467. routes[route]()
  468. }
  469. window.onhashchange()
  470. function goodPrice(){
  471. return Object.entries(store.getState().cart).map(i=>x+=i[1].count*i[1].good.price, x=0).reverse()[0] || 0
  472. }