index.js 18 KB

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