index.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. function createStore(reducer){
  2. let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
  3. let cbs = [] //массив подписчиков
  4. const getState = () => state //функция, возвращающая переменную из замыкания
  5. const subscribe = cb => (cbs.push(cb), //запоминаем подписчиков в массиве
  6. () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
  7. const dispatch = action => {
  8. if (typeof action === 'function'){ //если action - не объект, а функция
  9. return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
  10. }
  11. const newState = reducer(state, action) //пробуем запустить редьюсер
  12. if (newState !== state){ //проверяем, смог ли редьюсер обработать action
  13. state = newState //если смог, то обновляем state
  14. for (let cb of cbs) cb(state) //и запускаем подписчиков
  15. }
  16. }
  17. return {
  18. getState, //добавление функции getState в результирующий объект
  19. dispatch,
  20. subscribe //добавление subscribe в объект
  21. }
  22. }
  23. function combineReducers(reducers){
  24. function totalReducer(state={}, action){
  25. const newTotalState = {}
  26. for (const [reducerName, reducer] of Object.entries(reducers)){
  27. const newSubState = reducer(state[reducerName], action)
  28. if (newSubState !== state[reducerName]){
  29. newTotalState[reducerName] = newSubState
  30. }
  31. }
  32. if (Object.keys(newTotalState).length){
  33. return {...state, ...newTotalState}
  34. }
  35. return state
  36. }
  37. return totalReducer
  38. }
  39. function jwtDecode(token){
  40. try {
  41. return JSON.parse(atob(token.split('.')[1]))
  42. }
  43. catch(e){
  44. }
  45. }
  46. function localStoredReducer(reducer, localStorageKey){
  47. function wrapper(state, action){
  48. if (state === undefined){
  49. try {
  50. return JSON.parse(localStorage[localStorageKey])
  51. }
  52. catch(e){ }
  53. }
  54. const newState = reducer(state, action)
  55. localStorage.setItem(localStorageKey, JSON.stringify(newState))
  56. return newState
  57. }
  58. return wrapper
  59. }
  60. const reducers = {
  61. auth: authReducer,
  62. cart: localStoredReducer(cartReducer, 'cart'),
  63. promise: localStoredReducer(promiseReducer, 'promise'),
  64. }
  65. function promiseReducer(state={}, {type, status, payload, error,namePromise}){
  66. if (type === 'PROMISE'){
  67. return {
  68. ...state,
  69. [namePromise] : {status, payload, error}
  70. }
  71. }
  72. return state
  73. }
  74. const actionPending = (namePromise) => ({type: 'PROMISE', status: 'PENDING',namePromise})
  75. const actionFulfilled = (namePromise,payload) => ({type: 'PROMISE', status: 'FULFILLED',namePromise, payload})
  76. const actionRejected = (namePromise,error) => ({type: 'PROMISE', status: 'REJECTED', namePromise, error})
  77. const actionPromise = (namePromise,promise) =>
  78. async dispatch => {
  79. dispatch(actionPending(namePromise)) //сигнализируем redux, что промис начался
  80. try{
  81. const payload = await promise //ожидаем промиса
  82. dispatch(actionFulfilled(namePromise,payload)) //сигнализируем redux, что промис успешно выполнен
  83. return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса
  84. }
  85. catch (error){
  86. dispatch(actionRejected(namePromise,error)) //в случае ошибки - сигнализируем redux, что промис несложился
  87. }
  88. }
  89. const store = createStore(combineReducers(reducers)) //не забудьте combineReducers если он у вас уже есть
  90. store.subscribe(() => console.log(store.getState()))
  91. function authReducer(state={}, {type, token}){
  92. if (type === 'AUTH_LOGIN'){
  93. const payload = jwtDecode(token)
  94. try{
  95. if (payload){
  96. return {
  97. token,
  98. payload
  99. }
  100. }
  101. }
  102. catch (e) {}
  103. }
  104. if (type === 'AUTH_LOGOUT'){
  105. return {}
  106. }
  107. return state
  108. }
  109. const actionAuthLogout = () =>
  110. () => {
  111. store.dispatch({type: 'AUTH_LOGOUT'});
  112. localStorage.removeItem('authToken');
  113. }
  114. const actionAuthLogin = (token) =>
  115. () => {
  116. const oldState = store.getState()
  117. store.dispatch({type: 'AUTH_LOGIN', token})
  118. const newState = store.getState()
  119. if (oldState !== newState)
  120. localStorage.setItem('authToken', token)
  121. }
  122. function cartReducer(state={}, {type, count, good}){
  123. if(type==='CART_ADD'){
  124. if(typeof state[good._id]==='object'){
  125. let newCount = state[good._id].count+count
  126. return{
  127. ...state,
  128. [good._id]:{good:good, count:newCount}
  129. }
  130. }
  131. else{return{
  132. ...state,
  133. [good._id]:{good:good, count}
  134. }
  135. }
  136. }
  137. if(type==='CART_SET'){
  138. return{
  139. ...state,
  140. [good._id]:{good:good, count}
  141. }
  142. }
  143. if(type==='CART_SUB'){
  144. let newCount = state[good._id].count-count
  145. if(newCount>0){
  146. return{
  147. ...state,
  148. [good._id]:{good:good, count:newCount}
  149. }
  150. }else{
  151. delete state[good._id]
  152. }
  153. }
  154. if(type==='CART_DEL'){
  155. const {[good._id]: x,...newState} = state
  156. return newState
  157. }
  158. if(type==='CART_CLEAR'){
  159. return state={}
  160. }
  161. return state
  162. }
  163. const actionCartAdd = (good, count=1) => ({type: 'CART_ADD', count, good})
  164. const actionCartSub = (good, count=1) => ({type: 'CART_SUB', count, good})
  165. const actionCartDel = (good) => ({type: 'CART_DEL', good})
  166. const actionCartSet = (good, count=1) => ({type: 'CART_SET', count, good})
  167. const actionCartClear = () => ({type: 'CART_CLEAR'})
  168. const checkToken = () => {
  169. const headers = {
  170. "Content-Type": "application/json",
  171. "Accept": "application/json",
  172. }
  173. if(localStorage.getItem('authToken')) {
  174. return {
  175. ...headers,
  176. "Authorization": `Bearer ${localStorage.getItem('authToken')}`
  177. }
  178. } else {
  179. return headers;
  180. }
  181. }
  182. const getGQL = url =>
  183. (query, variables= {}) =>
  184. fetch(url, {
  185. method: 'POST',
  186. headers: checkToken(),
  187. body:JSON.stringify({query, variables})
  188. }).then(res => res.json())
  189. .then(data => {
  190. try {
  191. if(!data.data && data.errors) {
  192. throw new SyntaxError(`SyntaxError - ${JSON.stringify(Object.values(data.errors)[0])}`);
  193. }
  194. return Object.values(data.data)[0];
  195. } catch (e) {
  196. console.error(e);
  197. }
  198. });
  199. const url = 'http://shop-roles.node.ed.asmer.org.ua/'
  200. const gql = getGQL(url + 'graphql')
  201. const rootCats = () =>
  202. actionPromise('rootCats', gql(`query rootCats2{
  203. CategoryFind(query: "[{\\"parent\\": null}]"){
  204. _id
  205. name
  206. subCategories{_id name}
  207. }
  208. }`))
  209. store.dispatch(rootCats())
  210. const categoryGoods = (_id) =>
  211. actionPromise('categoryGoods', gql(`query categoryGoods ($q:String) {
  212. CategoryFindOne(query: $q) {
  213. _id
  214. name
  215. parent {
  216. _id
  217. name
  218. }
  219. subCategories {
  220. _id
  221. name
  222. }
  223. goods {
  224. _id
  225. name
  226. price
  227. description
  228. images {
  229. url
  230. }
  231. }
  232. }
  233. }`,
  234. {q: JSON.stringify([{_id}])}
  235. ))
  236. const Img = (_id) =>
  237. actionPromise('Img', gql(`query Img ($q:String) {
  238. GoodFindOne (query: $q){
  239. _id
  240. name
  241. price
  242. description
  243. images {
  244. url
  245. }
  246. }}`,
  247. {q: JSON.stringify([{_id}])}
  248. ))
  249. const actionRegister = (login, password) =>
  250. actionPromise('reg', gql(`mutation reg($login: String, $password: String) {
  251. UserUpsert(user: {login: $login, password: $password}) {
  252. _id
  253. createdAt
  254. }
  255. }`,
  256. {"login" : login,
  257. "password": password}
  258. ))
  259. const actionLogin = (login, password) =>
  260. actionPromise('login', gql(`query log($login:String, $password:String) {
  261. login(login:$login, password:$password)
  262. }`, {login, password}));
  263. const OrderHistory = () =>
  264. actionPromise('OrderHistory', gql(`query historyOfOrders {
  265. OrderFind(query:"[{}]") {
  266. _id
  267. total
  268. createdAt
  269. total
  270. }}`,
  271. {query: JSON.stringify([{}])}
  272. ))
  273. const orders = () =>
  274. gql(`query myOrders {
  275. OrderFind(query:"[{}]"){
  276. _id total orderGoods{
  277. price count total good{
  278. _id name images{
  279. url
  280. }
  281. }
  282. }
  283. }
  284. }`, {})
  285. const actionOrders = () => actionPromise('myOrders', orders())
  286. const actionOrder = () =>
  287. async (dispatch, getState) => {
  288. const order = Object.values(getState().cart).map(orderGoods => ({good: {_id: orderGoods.good._id}, count: orderGoods.count}));
  289. const newOrder = await dispatch(actionPromise('newOrder', gql(`mutation newOrder($order:OrderInput) {
  290. OrderUpsert(order:$order) {
  291. _id createdAt total
  292. }
  293. }`, {order: {orderGoods: order}})));
  294. if(newOrder) {
  295. dispatch(actionCartClear());
  296. basket()
  297. }
  298. }
  299. store.subscribe(() => {
  300. const {status, payload, error} = store.getState().promise.rootCats
  301. if (status === 'PENDING'){
  302. main.innerHTML = `<img src='https://flevix.com/wp-content/uploads/2020/01/Bounce-Bar-Preloader-1.gif' style="width: 500px;"/>`
  303. }
  304. if (status === 'FULFILLED'){
  305. aside.innerHTML = ''
  306. for (const {_id, name} of payload){
  307. aside.innerHTML += `<a href= "#/category/${_id}">${name}</a>`
  308. }
  309. }
  310. })
  311. store.subscribe(() => {
  312. const token = store.getState().auth.token
  313. if (jwtDecode(token)){
  314. reg.innerHTML='<a href="#/orderhistory">Мои Заказы</a>'
  315. login.innerHTML=`<button onclick="store.dispatch(actionAuthLogout())">Выйти</button>`
  316. }else{
  317. reg.innerHTML='<a href="#/register">Регистрация</a>'
  318. login.innerHTML='<a href="#/login">Логин</a>'
  319. }
  320. })
  321. store.subscribe(() => {
  322. const {status, payload} = store.getState().promise?.myOrders || {}
  323. const [,route] = location.hash.split('/')
  324. if(route !== 'orderhistory') {
  325. return
  326. }
  327. if (status === 'PENDING'){
  328. main.innerHTML = `<img src='https://flevix.com/wp-content/uploads/2020/01/Bounce-Bar-Preloader-1.gif' style="width: 500px;"/>`
  329. }
  330. if (status === 'FULFILLED'){
  331. main.innerHTML = ''
  332. let i = 1
  333. for (const goods of payload){
  334. let divOrders = document.createElement('div')
  335. divOrders.style="border: 2px solid #ebebeb;margin: 30px;"
  336. let numberOrder = document.createElement('h1')
  337. numberOrder.innerText=`Заказ № ${i}\n`
  338. divOrders.append(numberOrder)
  339. for(const obj of goods.orderGoods){
  340. const { price, count, total,good}=obj
  341. const {_id,name,}=good
  342. let div = document.createElement('div')
  343. let button = document.createElement('button')
  344. let a = document.createElement('a')
  345. let p = document.createElement('p')
  346. div.style="border: 2px solid #ebebeb;margin: 30px;"
  347. a.href=`#/good/${_id}`
  348. a.innerText=`${name}`
  349. p.innerText=`Стоимость товара ${price} грн\nКолличество ${count} шт\nСтоимость заказа ${total} грн\n`
  350. div.append(a,p)
  351. button.onclick= ()=>store.dispatch(actionCartAdd({_id: _id, price:price, name:name}))
  352. button.innerText='Добавить в корзину'
  353. div.append(button)
  354. divOrders.append(div)
  355. }
  356. main.prepend(divOrders)
  357. i++
  358. }
  359. let h1 = document.createElement('h1')
  360. h1.innerText='История Заказов'
  361. main.prepend(h1)
  362. }
  363. })
  364. store.subscribe(() => {
  365. const {status, payload, error} = store.getState().promise?.categoryGoods || {}
  366. const [,route] = location.hash.split('/')
  367. if(route !== 'category') {
  368. return
  369. }
  370. if (status === 'PENDING'){
  371. main.innerHTML = `<img src='https://flevix.com/wp-content/uploads/2020/01/Bounce-Bar-Preloader-1.gif' style="width: 500px;"/>`
  372. }
  373. if (status === 'FULFILLED'){
  374. main.innerHTML = ''
  375. const {name, goods} = payload
  376. main.innerHTML = `<h1>${name}</h1>`
  377. for (const good of goods){
  378. const {_id, name, price, images}=good
  379. let div = document.createElement('div')
  380. let button = document.createElement('button')
  381. let a = document.createElement('a')
  382. let p = document.createElement('p')
  383. div.style="border: 2px solid #ebebeb;margin: 30px;"
  384. a.href=`#/good/${_id}`
  385. a.innerText=`${name}`
  386. p.innerText=`${price} грн`
  387. div.append(a,p)
  388. for (const img of images) {
  389. let img1 = document.createElement('img')
  390. img1.src= `${url+ img.url}`
  391. div.append(img1)
  392. }
  393. button.onclick= ()=>store.dispatch(actionCartAdd(good))
  394. button.innerText='Добавить в корзину'
  395. div.append(button)
  396. main.append(div)
  397. }
  398. }
  399. })
  400. store.subscribe(() => {
  401. const {status, payload, error} = store.getState().promise?.Img || { }
  402. const [,route] = location.hash.split('/')
  403. if(route !== 'good') {
  404. return
  405. }
  406. if (status === 'PENDING'){
  407. main.innerHTML = `<img src='https://flevix.com/wp-content/uploads/2020/01/Bounce-Bar-Preloader-1.gif' style="width: 500px;"/>`
  408. }
  409. if (status === 'FULFILLED'){
  410. main.innerHTML = ''
  411. const {name,price,_id, description, images} = payload
  412. main.innerHTML = `<h1>${name}</h1>
  413. <p>${description}</p>`
  414. for (const img of images) {
  415. main.innerHTML += `<img src= "${url+ img.url}">`
  416. }
  417. main.innerHTML += `<p>${price} грн</p><br>`
  418. let button = document.createElement('button')
  419. button.onclick=()=> store.dispatch(actionCartAdd({_id: _id, price:price, name:name}))
  420. button.innerText=`Добавить в корзину`
  421. main.append(button)
  422. }
  423. })
  424. store.subscribe(() => {
  425. const {cart} = store.getState()
  426. let summ = 0
  427. for(const {count} of Object.values(cart)) {
  428. summ +=count
  429. }
  430. cartIcon.innerHTML = `<a href="#/cart"><b>Товаров в корзине: ${summ}</b></a>`
  431. })
  432. basket=() => {
  433. main.innerHTML = '<h1>Корзина</h1><br>'
  434. const {cart} = store.getState()
  435. let totalPrice = 0
  436. for(let {count,good} of Object.values(cart)) {
  437. const {name,price,_id}=good
  438. totalPrice += price*count
  439. let div = document.createElement('div')
  440. let a = document.createElement('a')
  441. let p = document.createElement('p')
  442. let button = document.createElement('button')
  443. let buttonCartSet = document.createElement('button')
  444. let input = document.createElement('input')
  445. input.type="number"
  446. div.style="border: 2px solid #ebebeb;margin-top: 15px;margin-bottom: 15px;"
  447. a.href= `#/good/${_id}`
  448. a.innerText=name
  449. p.innerText=`Стоимость ${price} грн\n Колличество ${count}\n Общая стоимость ${price*count} грн\n`
  450. buttonCartSet.innerHTML=`Изменить количество товара<br>`
  451. buttonCartSet.onclick= ()=>{
  452. store.dispatch(actionCartSet({_id: _id, price:price, name:name},count=Number(input.value)));
  453. basket()
  454. }
  455. button.onclick= ()=>{
  456. store.dispatch(actionCartDel({_id: _id, price:price, name:name}));
  457. basket()
  458. }
  459. button.innerText= 'Удалить товар'
  460. main.append(div)
  461. div.append(a,p,input)
  462. div.append(buttonCartSet,button)
  463. }
  464. let checkoutDiv = document.createElement('div')
  465. let buttonOrder = document.createElement('button')
  466. checkoutDiv.innerText= `К оплате ${totalPrice} грн \n`
  467. buttonOrder.innerText= `Оформить заказ`
  468. buttonOrder.onclick= ()=>{
  469. store.dispatch(actionOrder());
  470. basket()
  471. }
  472. main.append(checkoutDiv)
  473. checkoutDiv.append(buttonOrder)
  474. }
  475. function Password(parent, open) {
  476. let inputPass = document.createElement('input')
  477. inputPass.value='Пароль'
  478. let inputLogin = document.createElement('input')
  479. inputLogin.value='Логин'
  480. let checkBox = document.createElement('input')
  481. let button = document.createElement('button')
  482. button.innerText='Войти'
  483. button.disabled=true
  484. checkBox.type = 'checkbox'
  485. let div = document.createElement('div')
  486. this.divInnerText =(text)=> div.innerText= text
  487. this.buttonInnerText =(text)=> button.innerText= text
  488. this.setValue =(value)=> inputPass.value = value
  489. this.setOpen =(open)=> inputPass.type= open ?'text':'password'
  490. this.getValuePass =()=> inputPass.value
  491. this.getValueLogin =()=> inputLogin.value
  492. this.getOpen =()=> inputPass.type
  493. checkBox.onchange =()=>this.setOpen(checkBox.checked)
  494. function btn (){
  495. if(inputPass.value && inputLogin.value){
  496. button.disabled=false
  497. }
  498. else{
  499. button.disabled=true
  500. }
  501. }
  502. inputPass.oninput = btn
  503. inputLogin.oninput = btn
  504. this.getbutton =(func)=> button.onclick=func
  505. parent.append(inputLogin,inputPass,checkBox,button,div)
  506. }
  507. store.dispatch(actionAuthLogin(localStorage.authToken))
  508. window.onhashchange = () => {
  509. const [,route, _id] = location.hash.split('/')
  510. const routes = {
  511. category() {
  512. store.dispatch(categoryGoods(_id))
  513. },
  514. good(){
  515. store.dispatch(Img(_id))
  516. },
  517. cart(){
  518. basket();
  519. },
  520. orderhistory(){
  521. store.dispatch(actionOrders())
  522. },
  523. login(){
  524. main.innerHTML = '<h1>Вход</h1>'
  525. let windowLogin = new Password(main, false)
  526. windowLogin.buttonInnerText('Войти')
  527. windowLogin.getbutton(
  528. async () => {
  529. const token = await store.dispatch(actionLogin(windowLogin.getValueLogin(), windowLogin.getValuePass()))
  530. if (token){
  531. store.dispatch(actionAuthLogin(token));
  532. location.hash=''
  533. }
  534. })
  535. },
  536. register(){
  537. main.innerHTML = '<h1>Регистрация</h1>'
  538. let windowReg = new Password(main, false)
  539. windowReg.buttonInnerText('Зарегистрироваться')
  540. windowReg.getbutton(
  541. async () => {
  542. const user = await store.dispatch(actionRegister(windowReg.getValueLogin(), windowReg.getValuePass()))
  543. if(user) {
  544. const token = await store.dispatch(actionLogin(windowReg.getValueLogin(), windowReg.getValuePass()))
  545. if (token){
  546. store.dispatch(actionAuthLogin(token));
  547. location.hash=''
  548. }
  549. }
  550. })
  551. },
  552. }
  553. if (route in routes){
  554. routes[route]()
  555. }
  556. }
  557. window.onhashchange()