index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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() //и запускаем подписчиков
  15. }
  16. }
  17. return {
  18. getState, //добавление функции getState в результирующий объект
  19. dispatch,
  20. subscribe //добавление subscribe в объект
  21. }
  22. }
  23. function combineReducers(reducers) {
  24. return (state={}, action) => {
  25. const newState = {}
  26. // перебрать все редьюсеры
  27. if (reducers) {
  28. for (const [reducerName, reducer] of Object.entries(reducers)) {
  29. const newSubState = reducer(state[reducerName], action)
  30. if (newSubState !== state[reducerName]) {
  31. newState[reducerName] = newSubState
  32. }
  33. }
  34. // если newState не пустой, то вернуть стейт в
  35. if (Object.keys(newState).length !== 0) {
  36. return {...state, ...newState}
  37. } else {
  38. return state
  39. }
  40. }
  41. }
  42. }
  43. const combinedReducer = combineReducers({promise: promiseReducer, auth: authReducer})
  44. const store = createStore(combinedReducer)
  45. // console.log(store.getState()) // {promise: {}, auth: {}}
  46. function jwtDecode(token) {
  47. try {
  48. let decoded = JSON.parse(atob(token.split('.')[1]))
  49. return decoded
  50. } catch (err) {
  51. console.log(err)
  52. }
  53. }
  54. function authReducer(state, {type, token}) {
  55. if (!state) {
  56. if (localStorage.authToken) {
  57. token = localStorage.authToken
  58. type = 'AUTH_LOGIN'
  59. } else {
  60. return {}
  61. }
  62. }
  63. if (type === 'AUTH_LOGIN') {
  64. let payload = jwtDecode(token)
  65. if (typeof payload === 'object') {
  66. localStorage.authToken = token
  67. return {
  68. ...state,
  69. token,
  70. payload
  71. }
  72. } else {
  73. return state
  74. }
  75. }
  76. if (type === 'AUTH_LOGOUT') {
  77. delete localStorage.authToken
  78. return {}
  79. }
  80. return state
  81. }
  82. const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
  83. const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
  84. // const loginStore = createStore(authReducer)
  85. store.subscribe(() => console.log(store.getState()))
  86. // const inputToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2MWE0ZGIyOWM3NTBjMTJiYTZiYTQwMjIiLCJsb2dpbiI6ImVxd2VxZXdldyIsImFjbCI6WyI2MWE0ZGIyOWM3NTBjMTJiYTZiYTQwMjIiLCJ1c2VyIl19LCJpYXQiOjE2MzgxOTQ1NzZ9.Pi1GO6x7wdNrIrUKCQT-32-SsqmgFY-oFDrrXmw74-8'
  87. // loginStore.dispatch(actionAuthLogin(inputToken))
  88. // loginStore.dispatch(actionAuthLogout())
  89. // console.log(store.getState())
  90. function promiseReducer(state={}, {type, status, payload, error, name}) {
  91. if (!state) {
  92. return {}
  93. }
  94. if (type === 'PROMISE') {
  95. return {
  96. ...state,
  97. [name]: {
  98. status: status,
  99. payload : payload,
  100. error: error,
  101. }
  102. }
  103. }
  104. return state
  105. }
  106. const actionPending = (name) => ({type: 'PROMISE', status: 'PENDING', name})
  107. const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
  108. const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
  109. // const promiceStore = createStore(promiseReducer)
  110. // store.subscribe(() => console.log(store.getState()))
  111. // const delay = (ms) => new Promise((ok) => setTimeout(() => ok(ms), ms))
  112. // // store.dispatch(actionPending('delay1000'))
  113. // // delay(1000).then(data => store.dispatch(actionResolved('delay1000', data)),
  114. // // error => store.dispatch(actionRejected('delay1000', error)))
  115. // // store.dispatch(actionPending('delay2000'))
  116. // // delay(2000).then(data => store.dispatch(actionResolved('delay2000', data)),
  117. // // error => store.dispatch(actionRejected('delay2000', error)))
  118. const actionPromise = (name, promise) =>
  119. async (dispatch) => {
  120. dispatch(actionPending(name))
  121. try {
  122. let data = await promise
  123. dispatch(actionResolved(name, data))
  124. return data
  125. }
  126. catch(error){
  127. dispatch(actionRejected(name, error))
  128. }
  129. }
  130. const getGQL = url =>
  131. async (query, variables={}) => {
  132. // try {
  133. let obj = await fetch(url, {
  134. method: 'POST',
  135. headers: {
  136. "Content-Type": "application/json",
  137. ...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken} : {})
  138. },
  139. body: JSON.stringify({ query, variables })
  140. })
  141. let a = await obj.json()
  142. if (!a.data && a.errors) {
  143. throw new Error(JSON.stringify(a.errors))
  144. } else {
  145. return a.data[Object.keys(a.data)[0]]
  146. }
  147. // }
  148. // catch (error) {
  149. // console.log('Что-то не так, Бро ' + error);
  150. // }
  151. }
  152. const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua/'
  153. const gql = getGQL(backURL + 'graphql');
  154. const actionLogin = (login, password) => (
  155. actionPromise('login', gql(`query log($login: String, $password: String) {
  156. login(login: $login, password: $password)
  157. }`, {login, password}))
  158. )
  159. const actionFullLogin = (login, password) => (
  160. async (dispatch) => {
  161. let token = await dispatch(actionLogin(login, password))
  162. if (token) {
  163. // console.log(token)
  164. dispatch(actionAuthLogin(token))
  165. }
  166. }
  167. )
  168. const actionRegister = (login, password) => (
  169. actionPromise('register', gql(`mutation reg($user:UserInput) {
  170. UserUpsert(user:$user) {
  171. _id
  172. }
  173. }
  174. `, {user: {login, password}})
  175. )
  176. )
  177. const actionFullRegister = (login, password) => (
  178. async (dispatch) => {
  179. let regId = await dispatch(actionRegister(login, password))
  180. if (regId) {
  181. // console.log(regId)
  182. dispatch(actionFullLogin(login, password))
  183. }
  184. }
  185. )
  186. // regBtn.onclick = () => {
  187. // store.dispatch(actionFullRegister(loginReg.value, passReg.value))
  188. // }
  189. // loginBtn.onclick = () => {
  190. // store.dispatch(actionFullLogin(loginInput.value, passInput.value))
  191. // }
  192. // logoutBtn.onclick = () => {
  193. // store.dispatch(actionAuthLogout())
  194. // }
  195. const actionRootCats = () => (
  196. actionPromise('rootCats', gql(`query {
  197. CategoryFind(query: "[{\\"parent\\":null}]"){
  198. _id name
  199. }
  200. }`))
  201. )
  202. const actionCatById = (_id) => ( //добавить подкатегории
  203. actionPromise('catById', gql(`query catById($q: String){
  204. CategoryFindOne(query: $q){
  205. _id name goods {
  206. _id name price images {
  207. url
  208. }
  209. }
  210. subCategories {
  211. _id name
  212. }
  213. }
  214. }`, {q: JSON.stringify([{_id}])}))
  215. )
  216. // actionGoodById по аналогии
  217. const actionGoodById = (_id) => (
  218. actionPromise('goodById', gql(`query goodById($q: String) {
  219. GoodFindOne(query: $q) {
  220. _id name price description images {
  221. url
  222. }
  223. }
  224. }`, {q: JSON.stringify([{_id}])}))
  225. )
  226. const actionGoodsByUser = () => (
  227. actionPromise('goodByUser', gql(`query oUser($query: String) {
  228. OrderFind(query:$query){
  229. _id orderGoods{
  230. price count total good{
  231. _id name categories{
  232. name
  233. }
  234. images {
  235. url
  236. }
  237. }
  238. }
  239. owner {
  240. _id login
  241. }
  242. }
  243. }`,
  244. {query: JSON.stringify([{}])}))
  245. )
  246. store.dispatch(actionRootCats())
  247. store.subscribe(() => {
  248. const {promise} = store.getState()
  249. const {rootCats} = promise
  250. if (rootCats?.payload) {
  251. aside.innerHTML = ''
  252. const regBtn = document.createElement('a')
  253. regBtn.href = `#/register`
  254. regBtn.innerText = 'Register'
  255. const loginBtn = document.createElement('a')
  256. loginBtn.href = `#/login`
  257. loginBtn.innerText = 'Login'
  258. const logoutBtn = document.createElement('button')
  259. logoutBtn.innerText = 'Logout'
  260. aside.append(regBtn, loginBtn, logoutBtn)
  261. logoutBtn.onclick = () => {
  262. store.dispatch(actionAuthLogout())
  263. }
  264. for (const {_id, name} of rootCats?.payload) {
  265. const link = document.createElement('a')
  266. link.href = `#/category/${_id}`
  267. link.innerText = name
  268. aside.append(link)
  269. }
  270. }
  271. })
  272. // location.hash - адресная строка после решетки
  273. window.onhashchange = () => {
  274. const [,route, _id] = location.hash.split('/')
  275. const routes = {
  276. category(){
  277. store.dispatch(actionCatById(_id))
  278. console.log('страница категорий')
  279. },
  280. good(){ //задиспатчить actionGoodById
  281. store.dispatch(actionGoodById(_id))
  282. console.log('страница товара')
  283. },
  284. register(){
  285. let goRegister = createForm(main, 'Register', actionFullRegister)
  286. goRegister()
  287. },
  288. login(){
  289. let goLogin = createForm(main, 'Login', actionFullLogin)
  290. goLogin()
  291. },
  292. orders(){
  293. store.dispatch(actionGoodsByUser())
  294. }
  295. }
  296. if (route in routes) {
  297. routes[route]()
  298. }
  299. }
  300. function createForm(parent, type, callback) {
  301. parent.innerHTML = `
  302. <input id="login${type}" type="text"/>
  303. <input id="pass${type}" type="password"/>
  304. <button id="btn${type}">${type}</button>
  305. `
  306. return () => window[`btn${type}`].onclick = () => {
  307. store.dispatch(callback(window[`login${type}`].value, window[`pass${type}`].value))
  308. window[`pass${type}`].value = ''
  309. }
  310. }
  311. window.onhashchange()
  312. store.subscribe(() => {
  313. const {promise} = store.getState()
  314. const {catById} = promise
  315. const [,route, _id] = location.hash.split('/')
  316. if (catById?.payload && route === 'category'){
  317. const {name} = catById.payload;
  318. main.innerHTML = `<h1>${name}</h1>`
  319. if (catById.payload.subCategories) {
  320. for(const {_id, name} of catById.payload.subCategories) {
  321. const link = document.createElement('a');
  322. link.href = `#/category/${_id}`;
  323. link.innerText = name;
  324. main.append(link);
  325. }
  326. }
  327. if (catById.payload.goods) {
  328. for (const {_id, name, price, images} of catById.payload.goods){
  329. const card = document.createElement('div')
  330. card.innerHTML = `<h2>${name}</h2>
  331. <img src="${backURL}/${images[0].url}" />
  332. <strong>${price}</strong>
  333. <br>
  334. <a href="#/good/${_id}">Перейти на страницу товара</a>
  335. `
  336. main.append(card)
  337. }
  338. }
  339. }
  340. })
  341. store.subscribe(() => {
  342. const {promise} = store.getState()
  343. const {goodById} = promise
  344. const [,route, _id] = location.hash.split('/');
  345. if (goodById?.payload && route === 'good') {
  346. main.innerHTML = '';
  347. const {_id, name, images, price, description} = goodById.payload;
  348. const card = document.createElement('div');
  349. card.innerHTML = `<h2>${name}</h2>
  350. <img src="${backURL}/${images[0].url}" />
  351. <strong>${price}</strong>
  352. <h2>${description}</h2>
  353. `;
  354. main.append(card);
  355. }
  356. }
  357. //ТУТ ДОЛЖНА БЫТЬ ПРОВЕРКА НА НАЛИЧИЕ goodById в редакс
  358. //и проверка на то, что сейчас в адресной строке адрес ВИДА #/good/АЙДИ
  359. //в таком случае очищаем main и рисуем информацию про товар с подробностями
  360. )
  361. store.subscribe(() => {
  362. const {auth} = store.getState()
  363. const {payload} = auth
  364. if (payload?.sub ) {
  365. topContaner.innerHTML = ''
  366. const {id, login} = payload.sub
  367. const name = document.createElement('div')
  368. name.innerText = `ПРИВЕТ ${login}`
  369. topContaner.append(name)
  370. const myOrders = document.createElement('a')
  371. myOrders.innerText = 'Мои заказы'
  372. myOrders.href = `#/orders/${id}`
  373. topContaner.append(myOrders)
  374. } else {
  375. topContaner.innerHTML = ''
  376. }
  377. })
  378. store.subscribe(() => {
  379. const {promise} = store.getState()
  380. const {goodByUser} = promise
  381. const [,route] = location.hash.split('/')
  382. if (goodByUser?.payload && route === 'orders'){
  383. main.innerHTML = ''
  384. if (goodByUser.payload) {
  385. let totalMoney = 0
  386. for (const order of goodByUser.payload) {
  387. if (order.orderGoods) {
  388. for (const {price, count, total, good} of order.orderGoods) {
  389. if (price !== null && count !== null && total !== null && good !== null) {
  390. totalMoney += total
  391. const {_id, name, images} = good
  392. const card = document.createElement('div')
  393. card.innerHTML = `<h2>${name}</h2>
  394. <img src="${backURL}/${images[0].url}" />
  395. <strong>Куплено ${count} по ${price} грн. Итого:${total}</strong>
  396. <br>
  397. <a href="#/good/${_id}">Перейти на страницу товара</a>
  398. `
  399. main.append(card)
  400. }
  401. }
  402. }
  403. }
  404. const totalBlock = document.createElement('b')
  405. totalBlock.innerText = 'Итого потрачено: ' + totalMoney + ' грн'
  406. main.append(totalBlock)
  407. }
  408. }
  409. })
  410. // store.dispatch(actionPromise('delay1000', delay(1000)))
  411. // // store.dispatch(actionPromise('delay2000', delay(2000)))
  412. // // store.dispatch(actionPromise('luke', fetch('https://swapi.dev/api/people/1/').then(res => res.json())))