main.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  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 promiseReducer(state={}, {type, name, status, payload, error}){
  24. if (type === 'PROMISE'){
  25. return {
  26. ...state,
  27. [name]:{status, payload, error}
  28. }
  29. }
  30. return state
  31. }
  32. const jwtDecode = token => {
  33. try {
  34. let [,payload] = token.split(".");
  35. let payloadParsed= JSON.parse(atob(payload));
  36. return payloadParsed;
  37. }
  38. catch(e){
  39. console.log(e);
  40. }
  41. }
  42. function authReducer(state, {type, token}) {
  43. if (state === undefined) {
  44. if(localStorage.authToken) {
  45. type = "AUTH_LOGIN";
  46. token = localStorage.authToken;
  47. } else {
  48. return {};
  49. }
  50. }
  51. if (type === 'AUTH_LOGIN') {
  52. let payload = jwtDecode(token);
  53. if (payload) {
  54. localStorage.authToken = token;
  55. return {token, payload};
  56. }
  57. }
  58. if (type === 'AUTH_LOGOUT') {
  59. localStorage.authToken="";
  60. return {};
  61. }
  62. return state;
  63. }
  64. function combineReducers(reducers) {
  65. return (state = {}, action) => {
  66. const newState = {}
  67. for (const [reducerName, reducer] of Object.entries(reducers)) {
  68. let newSubState = reducer(state[reducerName], action)
  69. if (newSubState !== state[reducerName]) {
  70. newState[reducerName] = newSubState
  71. }
  72. }
  73. if (Object.keys(newState).length !== 0) {
  74. return { ...state, ...newState }
  75. } else {
  76. return state
  77. }
  78. }
  79. }
  80. function cartReducer(state={}, {type, good={}, count=1}) {
  81. //каков state
  82. //{
  83. // _id1: {count:1, good: {_id1, name, price, images}}
  84. // _id2: {count:1, good: {_id1, name, prica, images}}
  85. //}
  86. //
  87. const {_id} = good;
  88. if (type==='CART_ADD') {
  89. return {
  90. ...state,
  91. [_id]:{count: (state[_id]? (state[_id].count+=count) : count), good: good}
  92. }
  93. }
  94. if (type==='CART_CHANGE') {
  95. return {
  96. ...state,
  97. [_id]:{count: count, good: good}
  98. }
  99. }
  100. if (type==='CART_DELETE') {
  101. let newState = {... state};
  102. delete newState[_id];
  103. // const {_id, ...newState} = state;
  104. return {
  105. ...newState,
  106. }
  107. }
  108. if (type==='CART_CLEAR') {
  109. return {}
  110. }
  111. return state;
  112. }
  113. const store = createStore(combineReducers({promise: promiseReducer,
  114. auth: authReducer, cart: cartReducer}));
  115. const actionCartAdd = (good, count=1) => ({type: 'CART_ADD', good, count});
  116. const actionCartChange = (good, count=1) => ({type: 'CART_CHANGE', good, count});
  117. const actionCartDelete = (good) => ({type: 'CART_DELETE', good});
  118. const actionCartClear = () => ({type: 'CART_CLEAR'});
  119. const cartEl = document.getElementsByClassName("cartN")[0];
  120. let username;
  121. const loginEl = document.getElementsByClassName("loginQ")[0];
  122. const logoEl = document.getElementById("logo");
  123. // const store = createStore(promiseReducer) //не забудьте combineReducers если он у вас уже есть
  124. store.subscribe(() => console.log(store.getState()));
  125. const actionPending = name => ({type:'PROMISE',name, status: 'PENDING'});
  126. const actionFulfilled = (name,payload) => ({type:'PROMISE',name, status: 'FULFILLED', payload});
  127. const actionRejected = (name,error) => ({type:'PROMISE',name, status: 'REJECTED', error});
  128. const actionAuthLogin = (token) => ({ type: 'AUTH_LOGIN', token });
  129. const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' });
  130. const actionPromise = (name, promise) =>
  131. async dispatch => {
  132. dispatch(actionPending(name))
  133. try {
  134. let payload = await promise
  135. dispatch(actionFulfilled(name, payload))
  136. return payload
  137. }
  138. catch(error){
  139. dispatch(actionRejected(name, error))
  140. }
  141. }
  142. const getGQL = url =>
  143. (query, variables) => fetch(url, {
  144. method: 'POST',
  145. headers: {
  146. "Content-Type": "application/json",
  147. ...(localStorage.authToken ? {"Authorization": "Bearer " + localStorage.authToken} : {})
  148. },
  149. body: JSON.stringify({query, variables})
  150. }).then(res => res.json())
  151. .then(data => {
  152. if (data.data){
  153. return Object.values(data.data)[0]
  154. }
  155. else throw new Error(JSON.stringify(data.errors))
  156. })
  157. const backendURL = "http://shop-roles.node.ed.asmer.org.ua";
  158. const gql = getGQL(backendURL + '/graphql')
  159. const actionRootCats = () =>
  160. actionPromise('rootCats', gql(`query {
  161. CategoryFind(query: "[{\\"parent\\":null}]"){
  162. _id name
  163. }
  164. }`))
  165. const actionCatById = ( _id ) =>
  166. actionPromise(
  167. 'catById',
  168. gql(
  169. `query catById($q: String){
  170. CategoryFindOne(query: $q){
  171. subCategories{
  172. name _id goods {
  173. _id
  174. name
  175. }
  176. }
  177. _id name goods {
  178. _id name price images {
  179. url
  180. }
  181. }
  182. }
  183. }`, { q: JSON.stringify([{ _id }]) }));
  184. const actionGoodById = (_id) =>
  185. actionPromise('goodById', gql(`query goodz($q: String){
  186. GoodFindOne(query: $q) {
  187. _id name price description
  188. images{
  189. url
  190. }
  191. categories{
  192. name
  193. }
  194. }
  195. }`, {q: JSON.stringify([{_id}])}));
  196. const orderHistory = () =>
  197. actionPromise(
  198. 'history',
  199. gql(` query OrderFind{
  200. OrderFind(query:"[{}]"){
  201. _id total createdAt orderGoods{
  202. count good{
  203. _id name price images{
  204. url
  205. }
  206. }
  207. owner{
  208. _id login
  209. }
  210. }
  211. }
  212. }
  213. `));
  214. const actionLogin = (login, password) =>
  215. actionPromise(
  216. 'login',
  217. gql(
  218. `query log($login: String, $password: String) {
  219. login(login: $login, password: $password)
  220. }`,
  221. { login: login, password: password }
  222. )
  223. );
  224. const actionFullLogin = (login, password) => async (dispatch) => {
  225. let token = await dispatch(actionLogin(login, password));
  226. console.log(token);
  227. if (token) {
  228. dispatch(actionAuthLogin(token));
  229. }
  230. alerting();
  231. };
  232. const actionRegister = (login, password) =>
  233. actionPromise(
  234. 'register',
  235. gql(
  236. `mutation register ($login:String, $password:String){
  237. UserUpsert(user:{login:$login, password:$password}){
  238. _id login
  239. }
  240. }`,
  241. { login: login, password: password }
  242. )
  243. );
  244. const actionOrder = () => async (dispatch, getState) => {
  245. let { cart } = getState();
  246. const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({ good: { _id }, count, }));
  247. let result = await dispatch(
  248. actionPromise(
  249. 'order',
  250. gql(
  251. `
  252. mutation newOrder($order:OrderInput){
  253. OrderUpsert(order:$order)
  254. { _id total }
  255. }
  256. `,
  257. { order: { orderGoods } }
  258. )
  259. )
  260. );
  261. if (result?._id) {
  262. dispatch(actionCartClear());
  263. document.location.hash = "#/cart/";
  264. alert("Поздравляем, вы оформили заказ!");
  265. }
  266. };
  267. const actionFullRegister = (login, password) => async (dispatch) => {
  268. let id = await dispatch(actionRegister(login, password));
  269. // console.log(id);
  270. alertingReg(id, login);
  271. if (id) {
  272. dispatch(actionFullLogin(login, password));
  273. }
  274. };
  275. store.dispatch(actionRootCats());
  276. store.subscribe(() => {
  277. const {rootCats} = store.getState().promise
  278. if (rootCats?.payload){
  279. aside.innerHTML = ''
  280. for (const {_id, name} of rootCats?.payload){
  281. const link = document.createElement('a')
  282. link.href = `#/category/${_id}`
  283. link.innerText = name
  284. aside.append(link)
  285. }
  286. }
  287. })
  288. window.onhashchange = () => {
  289. const [, route, _id] = location.hash.split('/')
  290. const routes = {
  291. category(){
  292. store.dispatch(actionCatById(_id));
  293. },
  294. good(){
  295. // console.log("good")
  296. //задиспатчить actionGoodById
  297. // console.log('ТОВАРОСТРАНИЦА')
  298. store.dispatch(actionGoodById(_id));
  299. },
  300. login(){
  301. main.innerHTML = `<h2>Для осуществления заказов:<br>если вы зарегистрированы,
  302. введите логин и пароль и нажмите Войти;<br>если не зарегистрированы - нужно зарегистрироваться</h2>`;
  303. const formEl = document.createElement("div");
  304. const p1el = document.createElement("p");
  305. const p2el = document.createElement("p");
  306. const p3el = document.createElement("p");
  307. const br1El = document.createElement("br");
  308. const br2El = document.createElement("br");
  309. const br3El = document.createElement("br");
  310. const input1El = document.createElement("input");
  311. const input2El = document.createElement("input");
  312. const buttonLogEl = document.createElement("button");
  313. const buttonRegEl = document.createElement("button");
  314. const buttonLogOutEl = document.createElement("button");
  315. p1el.innerText = "Введите логин:";
  316. p2el.innerText = "Введите пароль:";
  317. input1El.id = "inputLogin";
  318. input2El.id = "inputPassword";
  319. input2El.type = "password";
  320. buttonLogEl.innerText = "Войти";
  321. buttonRegEl.innerText = "Зарегистрироваться";
  322. buttonLogOutEl.innerText = "Выйти";
  323. if(username) {
  324. buttonLogEl.disabled = true;
  325. buttonRegEl.disabled = true;
  326. }
  327. buttonLogEl.onclick = () => {
  328. store.dispatch(actionFullLogin(input1El.value, input2El.value));
  329. // if (username) location.hash = "#/goods";
  330. }
  331. buttonRegEl.onclick = () => {
  332. store.dispatch(actionFullRegister(input1El.value, input2El.value));
  333. }
  334. buttonLogOutEl.onclick = () => {
  335. store.dispatch(actionAuthLogout());
  336. username = "";
  337. document.location.hash = "#/login/";
  338. }
  339. formEl.append(p1el,input1El,p2el,input2El,br1El,br2El,buttonLogEl,buttonLogOutEl,buttonRegEl);
  340. main.append(formEl);
  341. //по кнопке - store.dispatch(actionFullLogin(login, password))
  342. },
  343. register(){
  344. //отрисовка тут
  345. //по кнопке - store.dispatch(actionFullRegister(login, password))
  346. },
  347. dashboard(){
  348. store.dispatch(orderHistory());
  349. },
  350. cart(){
  351. const cart = store.getState().cart;
  352. if(Object.entries(cart).length>0) {
  353. main.innerHTML = `<h2>Содержимое вашей корзины</h2>`;
  354. const {cart} = store.getState();
  355. let totalScore = 0;
  356. let totalCount = 0;
  357. const tableCartEl = document.createElement("table");
  358. const trEl = document.createElement("tr");
  359. const td1El = document.createElement("td");
  360. const td2El = document.createElement("td");
  361. const td3El = document.createElement("td");
  362. const td4El = document.createElement("td");
  363. const td5El = document.createElement("td");
  364. td1El.innerText = "Удалить";
  365. td2El.innerText = "Наименование товара";
  366. td3El.innerText = "Цена";
  367. td4El.innerText = "Количество";
  368. td5El.innerText = "Изменить";
  369. trEl.append(td1El,td2El,td3El,td4El,td5El);
  370. tableCartEl.append(trEl);
  371. if (Object.entries(cart).length>0) {
  372. for (let [key, value] of Object.entries(cart)) {
  373. const {count, good} = value;
  374. const {name, _id, price} = good;
  375. const buttonDel = document.createElement("button");
  376. const buttonMinus = document.createElement("button");
  377. const buttonPlus = document.createElement("button");
  378. buttonMinus.innerText = "-";
  379. buttonPlus.innerText = "+";
  380. buttonDel.innerText = "Удалить";
  381. buttonMinus.onclick = () => {
  382. if(count==1) {
  383. store.dispatch(actionCartDelete({_id, name, price}));
  384. } else {
  385. store.dispatch(actionCartChange({_id, name, price}, count-1));
  386. }
  387. document.location.hash = `#/cart/${_id}/${count}`
  388. }
  389. buttonPlus.onclick = () => {
  390. store.dispatch(actionCartChange({_id, name, price}, count+1));
  391. document.location.hash = `#/cart/${_id}/${count}`
  392. }
  393. buttonDel.onclick = () => {
  394. store.dispatch(actionCartDelete({_id, name, price}));
  395. document.location.hash = `#/cart/${_id}`;
  396. }
  397. const trEl = document.createElement("tr");
  398. const td1El = document.createElement("td");
  399. const td2El = document.createElement("td");
  400. const td3El = document.createElement("td");
  401. const td4El = document.createElement("td");
  402. const td5El = document.createElement("td");
  403. td1El.append(buttonDel);
  404. td2El.innerText = name;
  405. td3El.innerText = price;
  406. td4El.innerText = count;
  407. td5El.append(buttonMinus,buttonPlus);
  408. trEl.append(td1El,td2El,td3El,td4El,td5El);
  409. tableCartEl.append(trEl);
  410. totalScore+=price*count;
  411. totalCount+=count;
  412. }
  413. const trEl = document.createElement("tr");
  414. const td1El = document.createElement("td");
  415. const td2El = document.createElement("td");
  416. const td3El = document.createElement("td");
  417. const td4El = document.createElement("td");
  418. const td5El = document.createElement("td");
  419. td1El.innerText = "";
  420. td2El.innerText = "";
  421. td3El.innerText = totalScore;
  422. td4El.innerText = totalCount;
  423. td5El.innerText = "";
  424. trEl.append(td1El,td2El,td3El,td4El,td5El);
  425. tableCartEl.append(trEl);
  426. }
  427. main.append(tableCartEl);
  428. const buttonCartClear = document.createElement("button");
  429. const buttonSend = document.createElement("button");
  430. const buttonHistory = document.createElement("button");
  431. buttonSend.innerText = "Отправить заказ";
  432. buttonCartClear.innerText = "Очистить корзину";
  433. buttonHistory.innerText = "История заказов";
  434. buttonSend.onclick = () => {
  435. store.dispatch(actionOrder());
  436. }
  437. buttonCartClear.onclick = () => {
  438. store.dispatch(actionCartClear());
  439. document.location.hash = "#/cart/";
  440. }
  441. buttonHistory.onclick = () => {
  442. document.location.hash = "#/dashboard";
  443. }
  444. main.append(buttonSend,buttonCartClear, buttonHistory);
  445. } else {
  446. main.innerHTML = `<h2>Ваша корзина пуста...</h2><a href="#/dashboard">история заказов</a>`;
  447. }
  448. },
  449. }
  450. if (route in routes)
  451. routes[route]()
  452. }
  453. window.onhashchange()
  454. store.subscribe(() => {
  455. const {catById} = store.getState().promise;
  456. const [,route, _id] = location.hash.split('/');
  457. if (catById?.payload && route === 'category'){
  458. const {name} = catById.payload
  459. main.innerHTML = `<h1>${name}</h1>`
  460. if (catById.payload.subCategories) {
  461. for (const {_id, name} of catById.payload.subCategories){
  462. const card = document.createElement('div')
  463. card.innerHTML = `<a href="#/category/${_id}"><h2>${name}</h2></a>`;
  464. main.append(card);
  465. }
  466. }
  467. for (const {_id, name, price, images} of catById.payload.goods){
  468. const card = document.createElement('div')
  469. card.innerHTML = `<h2>${name}</h2>
  470. <img src="${backendURL}/${images[0].url}" /><br>
  471. <strong>Цена: ${price} грн</strong><br>
  472. <button onclick="store.dispatch(actionCartAdd({_id: '${_id}',
  473. name: '${name}', price: '${price}'}))">Купить</button>
  474. <a href="#/good/${_id}">описание товара</a>`
  475. main.append(card)
  476. }
  477. }
  478. })
  479. store.subscribe(() => {
  480. const {goodById} = store.getState().promise;
  481. const [,route, _id] = location.hash.split('/');
  482. //ТУТ ДОЛЖНА БЫТЬ ПРОВЕРКА НА НАЛИЧИЕ goodById в редакс
  483. //и проверка на то, что сейчас в адресной строке адрес ВИДА #/good/АЙДИ
  484. //в таком случае очищаем main и рисуем информацию про товар с подробностями
  485. if (goodById?.payload && route === 'good'){
  486. const {_id, name, price, description, images} = goodById.payload;
  487. main.innerHTML = `<h2>${name}</h2>`;
  488. const card = document.createElement('div');
  489. card.innerHTML = `<img src="${backendURL}/${images[0].url}" /><br>
  490. <strong>Цена: ${price} грн</strong><br>
  491. <button onclick="store.dispatch(actionCartAdd({_id: '${_id}',
  492. name: '${name}', price: '${price}'}))">Купить</button>
  493. <p>Описание товара:</p>
  494. <p>${description}</p>
  495. `
  496. main.append(card);
  497. }
  498. })
  499. store.subscribe(() => {
  500. const {cart} = store.getState();
  501. // console.log(cart);
  502. let numInCart = 0;
  503. if (Object.entries(cart).length>0) {
  504. for (let [key, value] of Object.entries(cart)) {
  505. const {count} = value;
  506. numInCart+=count;
  507. }
  508. }
  509. cartEl.innerText=numInCart;
  510. //достаем всю корзину
  511. //считаем общее количество всех позиций (3 айфона + 7 пицц = 10)
  512. //выводим число в кошик на странице через его HTML id
  513. });
  514. store.subscribe(() => {
  515. const {auth} = store.getState();
  516. // const [,route,] = location.hash.split('/');
  517. if(Object.keys(auth).length!==0) {
  518. username = auth?.payload.sub.login;
  519. }
  520. if(localStorage.authToken) {
  521. // console.log("login");
  522. loginEl.innerText = "";
  523. logoEl.innerHTML = `\n <h1>${username}, добро пожаловать в интернет магазин NotРОЗЕТКА</h1>\n `
  524. } else {
  525. // console.log("logout");
  526. loginEl.innerText = "?";
  527. logoEl.innerHTML = `\n <h1>Добро пожаловать в интернет магазин</h1>\n `
  528. }
  529. });
  530. //store.dispatch(actionPromise('delay1000', delay(1000)))
  531. //store.dispatch(actionPromise('delay2000', delay(2000)))
  532. //store.dispatch(actionPromise('failedfetch', fetch('https://swapi.dev/api/people/1/')
  533. //.then(res => res.json())))
  534. store.subscribe(() => {
  535. const {history} = store.getState().promise;
  536. const [,route] = location.hash.split('/');
  537. if (history?.payload && route === 'dashboard'){
  538. main.innerHTML = "<p><b>История ваших заказов:</b></p>";
  539. const card = document.createElement('div');
  540. for(let [key,value] of Object.entries(history.payload)) {
  541. const {_id, createdAt, total, orderGoods} = value;
  542. const p1El = document.createElement("p");
  543. const dateOfOrder = new Date(+createdAt);
  544. p1El.innerHTML = `${dateOfOrder.toLocaleDateString()} ${dateOfOrder.toLocaleTimeString()}
  545. Заказ ID: ${_id} от , c ${orderGoods.length} товарами на сумму ${total} грн`;
  546. card.append(p1El);
  547. }
  548. if (Object.keys(history.payload).length==0) {
  549. const p1El = document.createElement("p");
  550. p1El.innerHTML = "<p>Вы раньше еще ничего не заказывали</p>";
  551. card.append(p1El);
  552. }
  553. main.append(card);
  554. }
  555. });
  556. function alerting() {
  557. const {auth} = store.getState();
  558. if(Object.keys(auth).length!==0) {
  559. username = auth?.payload.sub.login;
  560. }
  561. if(username) {
  562. alert(`${username}, вы успешно зашли на сайт!`);
  563. document.location.hash = "#/category/5dc49f4d5df9d670df48cc64";
  564. } else {
  565. alert("Вы ввели неправильно логин или пароль!");
  566. }
  567. }
  568. function alertingReg(id, login) {
  569. if(id) {
  570. alert(`Вы успешно зарегистрировались с логином ${login}`);
  571. } else {
  572. alert(`Логин ${login} уже занят, придумайте другой!`);
  573. }
  574. }