script.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. cartIcon.addEventListener("click", () => {
  2. document.querySelector("#cartList").classList.remove("hide");
  3. });
  4. const backendURL = "http://shop-roles.asmer.fs.a-level.com.ua";
  5. //store
  6. function createStore(reducer) {
  7. let state = reducer(undefined, {}); //стартовая инициализация состояния, запуск редьюсера со state === undefined
  8. let cbs = []; //массив подписчиков
  9. const getState = () => state; //функция, возвращающая переменную из замыкания
  10. const subscribe = (cb) => (
  11. cbs.push(cb), //запоминаем подписчиков в массиве
  12. () => (cbs = cbs.filter((c) => c !== cb))
  13. ); //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
  14. const dispatch = (action) => {
  15. if (typeof action === "function") {
  16. return action(dispatch, getState); //запускаем эту функцию и даем ей dispatch и getState для работы
  17. }
  18. const newState = reducer(state, action); //пробуем запустить редьюсер
  19. if (newState !== state) {
  20. //проверяем, смог ли редьюсер обработать action
  21. state = newState; //если смог, то обновляем state
  22. for (let cb of cbs) cb(); //и запускаем подписчиков
  23. }
  24. };
  25. return {
  26. getState, //добавление функции getState в результирующий объект
  27. dispatch,
  28. subscribe, //добавление subscribe в объект
  29. };
  30. }
  31. const getGQL = (url) => (query, variables) =>
  32. fetch(url, {
  33. method: "POST",
  34. headers: {
  35. "Content-Type": "application/json",
  36. ...(localStorage.authToken
  37. ? { Authorization: "Bearer " + localStorage.authToken }
  38. : {}),
  39. },
  40. body: JSON.stringify({ query, variables }),
  41. })
  42. .then((res) => res.json())
  43. .then((data) => {
  44. if (data.data) {
  45. return Object.values(data.data)[0];
  46. } else throw new Error(JSON.stringify(data.errors));
  47. });
  48. const gql = getGQL(backendURL + "/graphql");
  49. function promiseReducer(state = {}, { type, name, status, payload, error }) {
  50. if (type === "PROMISE") {
  51. return {
  52. ...state, //.......скопировать старый state
  53. [name]: { status, payload, error }, //....... перекрыть в нем один name на новый объект со status, payload и error
  54. };
  55. }
  56. return state;
  57. }
  58. const actionPromise = (name, promise) => async (dispatch) => {
  59. dispatch(actionPending(name));
  60. try {
  61. let payload = await promise;
  62. dispatch(actionFulfilled(name, payload));
  63. return payload;
  64. } catch (error) {
  65. dispatch(actionRejected(name, error));
  66. }
  67. };
  68. //actions
  69. const actionPending = (name) => ({ type: "PROMISE", name, status: "PENDING" });
  70. const actionFulfilled = (name, payload) => ({
  71. type: "PROMISE",
  72. name,
  73. status: "FULFILLED",
  74. payload,
  75. });
  76. const actionRejected = (name, error) => ({
  77. type: "PROMISE",
  78. name,
  79. status: "REJECTED",
  80. error,
  81. });
  82. const actionRootCats = () =>
  83. actionPromise(
  84. "rootCats",
  85. gql(`query {
  86. CategoryFind(query: "[{\\"parent\\":null}]"){
  87. _id name
  88. }
  89. }`)
  90. );
  91. const actionCatById = (_id) =>
  92. actionPromise(
  93. "catById",
  94. gql(
  95. `query catById($q: String){
  96. CategoryFindOne(query: $q){
  97. _id name goods {
  98. _id name price images {
  99. url
  100. }
  101. }
  102. }
  103. }`,
  104. { q: JSON.stringify([{ _id }]) }
  105. )
  106. );
  107. const actionGoodById = (_id) =>
  108. actionPromise(
  109. "goodById",
  110. gql(
  111. `query goodById($q: String){
  112. GoodFindOne(query:$q){
  113. _id
  114. name
  115. price
  116. description
  117. categories{_id name}
  118. images{url}
  119. }
  120. } `,
  121. { q: JSON.stringify([{ _id }]) }
  122. )
  123. );
  124. const actionLogin = (login, password) =>
  125. actionPromise(
  126. "login",
  127. gql(
  128. `query login($login: String, $password: String){
  129. login(login: $login, password: $password)
  130. }`,
  131. { login: login, password: password }
  132. )
  133. );
  134. const actionRegister = (login, password) =>
  135. actionPromise(
  136. "register",
  137. gql(
  138. `mutation register($login:String, $password:String) {
  139. UserUpsert(user: {login:$login, password: $password}) {
  140. _id login
  141. }
  142. }`,
  143. { login: login, password: password }
  144. )
  145. );
  146. const actionAddToCard = (good, amount = 1) => ({
  147. type: "ADD_TO_CARD",
  148. good,
  149. amount,
  150. });
  151. const actionChangeAmount = (good, amount = 1) => ({
  152. type: "CHANGE_AMOUNT",
  153. good,
  154. amount,
  155. });
  156. const actionRemoveFromCard = (good, amount = 1) => ({
  157. type: "REMOVE_FROM_CARD",
  158. good,
  159. amount,
  160. });
  161. const actionRemoveCard = () => ({ type: "REMOVE_CARD" });
  162. const actionFullLogin = (login, password) => async (dispatch) => {
  163. let token = await dispatch(actionLogin(login, password));
  164. if (token) {
  165. dispatch(actionAuthLogin(token));
  166. }
  167. };
  168. const actionFullRegister = (login, password) => async (dispatch) => {
  169. try {
  170. await dispatch(actionRegister(login, password));
  171. } catch (error) {
  172. return console.log(error);
  173. }
  174. await dispatch(actionFullLogin(login, password));
  175. };
  176. const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
  177. const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
  178. const jwtDecode = (token) => {
  179. try {
  180. const payload = JSON.parse(atob(token.split(".")[1]));
  181. return payload;
  182. } catch (e) {}
  183. };
  184. //reducers
  185. function authReducer(state, { type, token }) {
  186. if (state === undefined && localStorage.authToken) {
  187. token = localStorage.authToken;
  188. type = "AUTH_LOGIN";
  189. }
  190. if (type === "AUTH_LOGIN") {
  191. // раскодируем токен
  192. let decode = jwtDecode(token); // (пишем отдельно функцию jwtDecode, и да будет в ней try-catch)
  193. if (decode) {
  194. // серединка, atob, JSON.parse
  195. localStorage.authToken = token; //если получилось пишем его в localStorage
  196. return { token, payload: decode }; // return{token, payload}
  197. }
  198. }
  199. if (type === "AUTH_LOGOUT") {
  200. localStorage.removeItem("authToken"); //чистим localStorage.authToken
  201. return {}; //возвращаем {}
  202. }
  203. return state || {};
  204. }
  205. function cartReducer(state = {}, { type, good = {}, amount = 1 }) {
  206. const id = good._id;
  207. const types = {
  208. ADD_TO_CARD() {
  209. return {
  210. ...state,
  211. [id]: { good, count: +amount + +(state[id] ? +state[id].count : 0) },
  212. };
  213. },
  214. CHANGE_AMOUNT() {
  215. return {
  216. ...state,
  217. [id]: { good, count: +amount },
  218. };
  219. },
  220. REMOVE_FROM_CARD() {
  221. let newState = { ...state };
  222. delete newState[id];
  223. return {
  224. ...newState,
  225. };
  226. },
  227. REMOVE_CARD() {
  228. return {};
  229. },
  230. };
  231. if (type in types) return types[type]();
  232. return state;
  233. }
  234. function combineReducers(reducers) {
  235. return (state = {}, action) => {
  236. const newState = {};
  237. for (const [nameOfReducer, currentReducer] of Object.entries(reducers)) {
  238. let newCurrentState = currentReducer(state[nameOfReducer], action);
  239. if (newCurrentState !== state[nameOfReducer]) {
  240. newState[nameOfReducer] = newCurrentState;
  241. }
  242. }
  243. return Object.keys(newState).length !== 0
  244. ? { ...state, ...newState }
  245. : state;
  246. };
  247. }
  248. const combinedReducer = combineReducers({
  249. goods: promiseReducer,
  250. auth: authReducer,
  251. cart: cartReducer,
  252. });
  253. const store = createStore(combinedReducer);
  254. store.dispatch(actionAuthLogout());
  255. store.dispatch(actionRootCats());
  256. //subscribes
  257. store.subscribe(() => console.log(store.getState()));
  258. store.subscribe(() => {
  259. const { rootCats } = store.getState().goods;
  260. if (rootCats?.payload) {
  261. aside.innerHTML = "";
  262. for (const { _id, name } of rootCats.payload) {
  263. const link = document.createElement("a");
  264. link.href = `#/category/${_id}`;
  265. link.innerText = name;
  266. aside.append(link);
  267. }
  268. }
  269. });
  270. store.subscribe(() => {
  271. const { catById } = store.getState().goods;
  272. const [, route, _id] = location.hash.split("/");
  273. if (catById?.payload && route === "category") {
  274. const { name } = catById.payload;
  275. main.innerHTML = `<h1>${name}</h1>`;
  276. for (const { _id, name, price, images } of catById.payload.goods) {
  277. const card = document.createElement("div");
  278. card.innerHTML = `<h2>${name}</h2>
  279. <img src="${backendURL}/${images[0].url}" />
  280. <strong>Цена ${price} грн</strong>
  281. <a href="#/good/${_id}">Перейти</a> `; // ТУТ ДОЛЖНА БЫТЬ ССЫЛКА НА СТРАНИЦУ ТОВАРА ВИДА #/good/АЙДИ
  282. main.append(card);
  283. }
  284. }
  285. });
  286. store.subscribe(() => {
  287. //ТУТ ДОЛЖНА БЫТЬ ПРОВЕРКА НА НАЛИЧИЕ goodById в редакс
  288. const { goodById } = store.getState().goods;
  289. const [, route, _id] = location.hash.split("/");
  290. if (goodById?.payload && route === "good") {
  291. const { name, images, price, description } = goodById.payload;
  292. main.innerHTML = `<h1>${name}</h1> Товар`;
  293. const card = document.createElement("div");
  294. card.innerHTML = `<h2>${name}</h2>
  295. <img src ="${backendURL}/${images[0].url}"/>
  296. <strong>Цена : ${price} грн</strong>
  297. <button class='buy buy${_id}'>Купить</button>
  298. <p>${description}</p>
  299. `;
  300. main.append(card);
  301. document.querySelector(`.buy${_id}`).addEventListener("click", () => {
  302. store.dispatch(actionAddToCard(goodById.payload));
  303. });
  304. }
  305. });
  306. store.subscribe(() => {
  307. const cartState = store.getState().cart;
  308. cartList.innerHTML = `<div id='closeListBtn'>X</div>`;
  309. closeListBtn.addEventListener("click", () => {
  310. document.querySelector("#cartList").classList.add("hide");
  311. });
  312. let sum = 0;
  313. for (const id in cartState) {
  314. const { count, good } = cartState[id];
  315. const cartListItem = document.createElement("div");
  316. cartListItem.classList.add("cartListItem");
  317. cartListItem.innerHTML += `<p>- ${good.name}: </p>`;
  318. const inputCount = document.createElement("input");
  319. inputCount.type = "number";
  320. inputCount.value = count;
  321. inputCount.addEventListener("change", (e) => {
  322. store.dispatch(actionChangeAmount(good, e.target.value));
  323. });
  324. cartListItem.appendChild(inputCount);
  325. const goodPrice = document.createElement("p");
  326. goodPrice.innerText = `цена: ${good.price}`;
  327. cartListItem.appendChild(goodPrice);
  328. const goodCost = document.createElement("strong");
  329. goodCost.innerText = `всего ${+good.price * +count} грн.`;
  330. sum += +good.price * +count;
  331. cartListItem.appendChild(goodCost);
  332. const removeBtn = document.createElement("button");
  333. removeBtn.innerText = "remove";
  334. removeBtn.addEventListener("click", () => {
  335. store.dispatch(actionRemoveFromCard(good));
  336. });
  337. cartListItem.appendChild(removeBtn);
  338. cartList.appendChild(cartListItem);
  339. }
  340. if (cartList.querySelectorAll(".cartListItem").length) {
  341. const btnToOrder = document.createElement("button");
  342. btnToOrder.innerText = "Заказать";
  343. btnToOrder.classList = "btnToOrder";
  344. cartList.appendChild(btnToOrder);
  345. btnToOrder.addEventListener("click", () => {
  346. // store.dispatch(actionToOrder());
  347. console.log("Ваш заказ в процессе обработки");
  348. });
  349. const priceOfOrder = document.createElement("div");
  350. priceOfOrder.classList = "priceOfOrder";
  351. priceOfOrder.innerText = `Итого : ${sum}`;
  352. cartList.appendChild(priceOfOrder);
  353. const btnDeleteAll = document.createElement("button");
  354. btnDeleteAll.classList = "btnDeleteAll";
  355. btnDeleteAll.innerText = "Очистить всё";
  356. btnDeleteAll.addEventListener("click", () => {
  357. store.dispatch(actionRemoveCard());
  358. });
  359. cartList.appendChild(btnDeleteAll);
  360. }
  361. });
  362. window.onhashchange = () => {
  363. const [, route, _id] = location.hash.split("/");
  364. const routes = {
  365. category() {
  366. store.dispatch(actionCatById(_id));
  367. },
  368. good() {
  369. //задиспатчить actionGoodById
  370. store.dispatch(actionGoodById(_id));
  371. },
  372. login() {
  373. loginBlock.classList.add("hide");
  374. const innerContent = `
  375. <div class="loginWrapper">
  376. <input class="login">
  377. <input type="password" class="password">
  378. <button class="logInButton">LogIn</button>
  379. </div>
  380. `;
  381. header.innerHTML = innerContent + header.innerHTML;
  382. const inpLogIn = document.querySelector(".login");
  383. const inpPassword = document.querySelector(".password");
  384. const loginWrapper = document.querySelector(".loginWrapper");
  385. document
  386. .querySelector(".logInButton")
  387. .addEventListener("click", async () => {
  388. if (inpLogIn.value.length && inpPassword.value.length) {
  389. await store.dispatch(
  390. actionFullLogin(inpLogIn.value, inpPassword.value)
  391. );
  392. const login = store.getState().auth.payload.sub.login;
  393. userData.innerHTML = `<p>Привет, ${login}</p>`;
  394. header.removeChild(loginWrapper);
  395. const btnLogout = document.createElement("button");
  396. btnLogout.textContent = "Exit";
  397. btnLogout.addEventListener("click", () => {
  398. userData.innerHTML = "";
  399. store.dispatch(actionAuthLogout());
  400. loginBlock.style.display = "block";
  401. oders.classList.add("hide");
  402. });
  403. orders.classList.remove("hide");
  404. userData.append(btnLogout);
  405. }
  406. });
  407. },
  408. register() {
  409. loginBlock.classList.add("hide");
  410. const innerContent = `
  411. <div class="loginWrapper">
  412. <input class="loginRegister">
  413. <input type="password" class="passwordRegister">
  414. <button class="registrationButton">Registration</button>
  415. </div>
  416. `;
  417. header.innerHTML = innerContent + header.innerHTML;
  418. const loginRegister = document.querySelector(".loginRegister");
  419. const passwordRegister = document.querySelector(".passwordRegister");
  420. document
  421. .querySelector(".registrationButton")
  422. .addEventListener("click", () => {
  423. if (loginRegister.value.length && passwordRegister.value.length) {
  424. store.dispatch(
  425. actionFullRegister(loginRegister.value, passwordRegister.value)
  426. );
  427. loginBlock.classList.remove("hide");
  428. }
  429. });
  430. },
  431. };
  432. if (route in routes) routes[route]();
  433. };
  434. window.onhashchange();