index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. let signin = document.querySelector('#signin');
  2. let register = document.querySelector('#register');
  3. let cartBtn = document.querySelector('#cart-btn');
  4. let logRegisterInput = document.querySelector('#signup');
  5. let passRegisterInput = document.querySelector('#signup-password');
  6. let btnRegister = document.querySelector('#btn-register');
  7. let loginInput = document.querySelector('#login');
  8. let passwordInput = document.querySelector('#login-password');
  9. let btnLogin = document.querySelector('#btn-login');
  10. let overlay = document.querySelector(".overlay");
  11. let registerForm = document.querySelector('#signup-form');
  12. let loginForm = document.querySelector('#login-form');
  13. let cartWrap = document.querySelector('#cart-wrap');
  14. let dashboardWrap = document.querySelector('#dashboard-wrap');
  15. let Closes = document.querySelectorAll(".close");
  16. let registerWrap = document.querySelector("#register-wrap");
  17. let userLogoutWrap = document.querySelector("#userlogout-wrap");
  18. let user = document.querySelector("#user");
  19. let dashboardUl = document.querySelector("#dashboard")
  20. let cartUl = document.querySelector("#cart");
  21. let btnBuy = document.querySelector("#btn-buy");
  22. let logout = document.querySelector('#logout');
  23. let dashboardBtn = document.querySelector('#dashboard-btn');
  24. function showAndHideElem(element, value){
  25. element.style.display = value;
  26. overlay.style.display = value
  27. }
  28. signin.addEventListener("click", () => {
  29. showAndHideElem(loginForm, 'block');
  30. });
  31. register.addEventListener("click", () => {
  32. showAndHideElem(registerForm, 'block');
  33. })
  34. cartBtn.addEventListener("click", () => {
  35. showAndHideElem(cartWrap, 'block')
  36. })
  37. dashboardBtn.addEventListener("click", () => {
  38. showAndHideElem(dashboardWrap, 'block')
  39. })
  40. Closes[0].addEventListener("click", () => {
  41. showAndHideElem(registerForm, 'none');
  42. registerForm.reset();
  43. })
  44. Closes[1].addEventListener("click", () => {
  45. showAndHideElem(loginForm, 'none');
  46. loginForm.reset();
  47. })
  48. Closes[2].addEventListener("click", () => {
  49. showAndHideElem(cartWrap, 'none')
  50. })
  51. Closes[3].addEventListener("click", () => {
  52. showAndHideElem(dashboardWrap, 'none')
  53. })
  54. function createStore(reducer){
  55. let state = reducer(undefined, {})
  56. let cbs = []
  57. const getState = () => state
  58. const subscribe = cb => (cbs.push(cb),
  59. () => cbs = cbs.filter(c => c !== cb))
  60. const dispatch = action => {
  61. if (typeof action === 'function'){
  62. return action(dispatch, getState)
  63. }
  64. const newState = reducer(state, action)
  65. if (newState !== state){
  66. state = newState
  67. for (let cb of cbs) cb()
  68. }
  69. }
  70. return {
  71. getState,
  72. dispatch,
  73. subscribe
  74. }
  75. }
  76. const getGQL = url =>
  77. (query, variables) => fetch(url, {
  78. method: 'POST',
  79. headers: {
  80. "Content-Type": "application/json",
  81. // 'Accept' : 'application/json',
  82. ...(localStorage.authToken ? {"Authorization": "Bearer " + localStorage.authToken} : {})
  83. },
  84. body: JSON.stringify({query, variables})
  85. }).then(res => res.json())
  86. .then(data => {
  87. if (data.data){
  88. return Object.values(data.data)[0]
  89. }
  90. else throw new Error(JSON.stringify(data.errors))
  91. })
  92. const backendURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
  93. const gql = getGQL(backendURL + '/graphql');
  94. const jwtDecode = token => {
  95. try{
  96. return JSON.parse(atob(token.split('.')[1]));
  97. }
  98. catch(e){
  99. console.log(e.name, e.message);
  100. }
  101. }
  102. function promiseReducer(state={}, {type, name, status, payload, error}){
  103. if (type === 'PROMISE'){
  104. return {
  105. ...state,
  106. [name]:{status, payload, error}
  107. }
  108. }
  109. return state
  110. }
  111. function authReducer(state, {type, token}){
  112. if (state === undefined){
  113. if(localStorage.authToken){
  114. type = 'AUTH_LOGIN';
  115. token = localStorage.authToken
  116. }
  117. }
  118. if(type === 'AUTH_LOGIN'){
  119. let payload = jwtDecode(token);
  120. if (payload){
  121. localStorage.authToken = token
  122. return {token, payload}
  123. }
  124. }
  125. if(type === 'AUTH_LOGOUT'){
  126. localStorage.removeItem("authToken")
  127. return {}
  128. }
  129. return state || {}
  130. }
  131. const combineReducers = (reducers) => (state={}, action) => {
  132. let newState = {}
  133. for (const [reducerName, reducer] of Object.entries(reducers)){
  134. let subNewState = reducer(state[reducerName],action)
  135. if(subNewState !== state[reducerName]){
  136. newState = {
  137. ...newState, [reducerName] : subNewState
  138. }
  139. }
  140. }
  141. if(Object.keys(newState).length > 0){
  142. return {
  143. ...state,...newState
  144. }
  145. }
  146. return state
  147. }
  148. function cartReducer(state = {}, {type, good, count=1}){
  149. //каков state:
  150. //{
  151. // _id1: {count:1, good: {_id1, name, price, images}}
  152. // _id2: {count:1, good: {_id2, name, price, images}}
  153. //}
  154. //каковы действия по изменению state
  155. if (type === 'CART_ADD'){
  156. return {
  157. ...state,
  158. [good._id]: {count: count+(state[good._id]?.count || 0), good : good}
  159. //копируем старое и подменяем один ключ на новое, однако если
  160. //ключ был, берем count из старого и прибавляем к count из action.
  161. }
  162. }
  163. if (type === 'CART_CHANGE'){
  164. return {
  165. ...state,
  166. [good._id] : {count: count, good : good}
  167. // ///!меняем полностью
  168. // //копируем старое и подменяем один ключ на новое. аналогично ларьку
  169. // //и promiseReducer
  170. }
  171. }
  172. if (type === 'CART_DELETE'){
  173. let {[good._id]: id1, ...newState} = state;
  174. return {
  175. ...newState
  176. }
  177. //смочь скопировать объект state без одного ключа
  178. //(для этого есть хитрая деструктуризация, например
  179. //вернуть копию без этого ключа
  180. }
  181. if (type === 'CART_CLEAR'){
  182. return {}
  183. }
  184. return state
  185. }
  186. const actionCartAdd = (good, count=1) => ({type: 'CART_ADD', good, count})
  187. const actionCartChange = (good, count=1) => ({type: 'CART_CHANGE', good, count}) ///oninput меняяем полностью
  188. const actionCartDelete = (good) => ({type: 'CART_DELETE', good})
  189. const actionCartClear = () => ({type: 'CART_CLEAR'})
  190. const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}));
  191. store.subscribe(() => console.log(store.getState()))
  192. const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token});
  193. const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'});
  194. const actionPending = name => ({type:'PROMISE',name, status: 'PENDING'})
  195. const actionFulfilled = (name,payload) => ({type:'PROMISE',name, status: 'FULFILLED', payload})
  196. const actionRejected = (name,error) => ({type:'PROMISE',name, status: 'REJECTED', error})
  197. const actionPromise = (name, promise) =>
  198. async (dispatch) => {
  199. dispatch(actionPending(name))
  200. try {
  201. let payload = await promise;
  202. dispatch(actionFulfilled(name, payload));
  203. return payload
  204. }
  205. catch(error){
  206. dispatch(actionRejected(name, error))
  207. }
  208. }
  209. const actionRootCats = () =>
  210. actionPromise('rootCats', gql(`query {
  211. CategoryFind(query: "[{\\"parent\\":null}]"){
  212. _id name
  213. }
  214. }`))
  215. const actionCatById = (_id) => //добавить подкатегории
  216. actionPromise('catById', gql(`query catById($q: String){
  217. CategoryFindOne(query: $q){
  218. _id name subCategories {
  219. name _id
  220. }
  221. goods {
  222. _id name price images {
  223. url
  224. }
  225. }
  226. }
  227. }`, {q: JSON.stringify([{_id}])}))
  228. const actionGoodById = (_id) =>
  229. actionPromise('goodById', gql(`query goodByid($goodId: String) {
  230. GoodFindOne(query: $goodId) {
  231. name
  232. price
  233. description
  234. images {
  235. url
  236. }
  237. }
  238. }`, {goodId: JSON.stringify([{_id}])}))
  239. const actionFullRegister = (log, pass) =>
  240. async dispatch => {
  241. let user = await dispatch(
  242. actionPromise('register', gql( `mutation register($login: String, $password: String) {
  243. UserUpsert(user: {login: $login, password: $password}) {
  244. _id
  245. login
  246. }
  247. }`, {login : log, password : pass}))
  248. )
  249. if(user){
  250. dispatch(actionFullLogin(log, pass));
  251. }
  252. }
  253. const actionFullLogin = (log, pass) =>
  254. async dispatch => {
  255. let token = await dispatch(
  256. actionPromise('login', gql(`query login($login: String, $password: String) {
  257. login(login: $login, password: $password)
  258. }`, {login: log, password: pass}))
  259. )
  260. if(token){
  261. dispatch(actionAuthLogin(token))
  262. }
  263. }
  264. const actionNewOrder = () =>
  265. async (dispatch, getState) => {
  266. const {cart} = getState();
  267. let order = {orderGoods : []}
  268. for(let [key, value] of Object.entries(cart)){
  269. let newValue = {...value}
  270. let {name,price,images, ...id} = newValue.good;
  271. newValue.good = id;
  272. order.orderGoods.push({...newValue})
  273. }
  274. let newOrder = await dispatch(
  275. actionPromise('newOrder', gql(`mutation newOrder($order: OrderInput) {
  276. OrderUpsert(order: $order) {
  277. _id
  278. total
  279. }
  280. }`, {order: order}))
  281. )
  282. if(newOrder){
  283. dispatch(actionCartClear())
  284. }
  285. }
  286. const actionOrders = () =>
  287. actionPromise('orders', gql(`query findOrder($q: String) {
  288. OrderFind(query: $q) {
  289. _id
  290. total
  291. createdAt
  292. orderGoods {
  293. count
  294. good {
  295. name
  296. price
  297. }
  298. }
  299. }
  300. }`, {q: JSON.stringify([{}])}));
  301. store.dispatch(actionRootCats())
  302. store.subscribe(() => {
  303. const {rootCats} = store.getState().promise
  304. if (rootCats?.payload){
  305. aside.innerHTML = ''
  306. for (const {_id, name} of rootCats.payload){
  307. const link = document.createElement('a')
  308. link.href = `#/category/${_id}`
  309. link.innerText = name
  310. aside.append(link)
  311. }
  312. }
  313. })
  314. window.onhashchange = () => {
  315. const [, route, _id] = location.hash.split('/');
  316. console.log()
  317. const routes = {
  318. category(){
  319. store.dispatch(actionCatById(_id));
  320. console.log('work')
  321. },
  322. good(){ //задиспатчить actionGoodById
  323. store.dispatch(actionGoodById(_id))
  324. },
  325. login(){
  326. //отрисовка тут
  327. btnLogin.onclick = (e) => {e.preventDefault() ;store.dispatch(actionFullLogin(loginInput.value, passwordInput.value))};
  328. },
  329. register(){
  330. btnRegister.onclick = (e) => {e.preventDefault(); store.dispatch(actionFullRegister(logRegisterInput.value, passRegisterInput.value))}
  331. },
  332. dashboard(){ //#/dashboard
  333. //задиспатчить actionOrders
  334. store.dispatch(actionOrders())
  335. console.log('заказостраница')
  336. }
  337. }
  338. if (route in routes)
  339. routes[route]()
  340. }
  341. window.onhashchange()
  342. store.subscribe(() => {
  343. const {catById} = store.getState().promise
  344. const [,route, _id] = location.hash.split('/')
  345. if (catById?.payload && route === 'category'){
  346. const {name, subCategories} = catById.payload
  347. main.innerHTML = `<h1 class="category-name">${name}</h1>`
  348. if(subCategories){
  349. for(let {name, _id} of subCategories){
  350. const link = document.createElement('a')
  351. link.href = `#/category/${_id}`
  352. link.innerText = name;
  353. main.append(link)
  354. }
  355. }
  356. for (const {_id, name, price, images} of catById.payload.goods){
  357. const card = document.createElement('div')
  358. card.innerHTML = `<h2>${name}</h2>
  359. <img src="${backendURL}/${images[0].url}" />
  360. <strong>${price}</strong><br>
  361. <a href="#/good/${_id}">посмотреть на ${name}</a>
  362. `
  363. main.append(card);
  364. let btnAddToCart = document.createElement('button');
  365. btnAddToCart.classList.add('btn-buy');
  366. btnAddToCart.innerText = 'Добавить в корзину'
  367. card.append(btnAddToCart);
  368. btnAddToCart.onclick = () => store.dispatch(actionCartAdd({_id: _id, name: name, price: price, images: images}))
  369. }
  370. }
  371. })
  372. store.subscribe(() => {
  373. const {goodById} = store.getState().promise
  374. const [,route, _id] = location.hash.split('/')
  375. if(goodById?.payload && route === 'good'){
  376. const {name, price, description, images} = goodById.payload;
  377. main.innerHTML = `<h1>${name}</h1>`
  378. const card = document.createElement('div');
  379. card.innerHTML = `<img src="${backendURL}/${images[0].url}" />
  380. <strong>${price}</strong><br>
  381. <div>${description}</div>
  382. `
  383. main.append(card);
  384. let btnAddToCart = document.createElement('button');
  385. btnAddToCart.classList.add('btn-buy');
  386. btnAddToCart.innerText = 'Добавить в корзину'
  387. card.append(btnAddToCart);
  388. btnAddToCart.onclick = () => store.dispatch(actionCartAdd({_id: _id, name: name, price: price, images: images}))
  389. }
  390. //ТУТ ДОЛЖНА БЫТЬ ПРОВЕРКА НА НАЛИЧИЕ goodById в редакс
  391. //и проверка на то, что сейчас в адресной строке адрес ВИДА #/good/АЙДИ
  392. //в таком случае очищаем main и рисуем информацию про товар с подробностями
  393. //....А ТАК ЖЕ КНОПКА Купить, которая диспатчит actionCartAdd
  394. })
  395. store.subscribe(() => { //если залогинен отрисовать юзернейм и кнопку логаут
  396. const {payload} = store.getState().auth
  397. if(payload?.sub){
  398. registerWrap.style.display = 'none';
  399. userLogoutWrap.style.display = 'block';
  400. const {login} = payload.sub;
  401. user.innerHTML = login;
  402. } else {
  403. registerWrap.style.display = 'block';
  404. userLogoutWrap.style.display = 'none';
  405. }
  406. })
  407. store.subscribe(() => {
  408. cartUl.innerHTML = ''
  409. const {cart} = store.getState();
  410. for (let value of Object.values(cart)){
  411. const {count, good} = value;
  412. const li = document.createElement("li");
  413. li.innerHTML = `<img src="${backendURL}/${good.images[0].url}"/>
  414. <strong>${good.name}</strong>
  415. <strong>${count}</strong>
  416. `
  417. cartUl.append(li);
  418. const input = document.createElement("input");
  419. li.append(input);
  420. input.value = count
  421. input.oninput = () => store.dispatch(actionCartChange(good, +input.value));
  422. const button = document.createElement("button");
  423. button.innerText = 'удалить';
  424. li.append(button);
  425. button.onclick = () => store.dispatch(actionCartDelete(good))
  426. }
  427. })
  428. store.subscribe(() => {
  429. const {cart} = store.getState();
  430. Object.keys(cart).length > 0 ?
  431. btnBuy.style.display='block' :
  432. btnBuy.style.display='none';
  433. btnBuy.onclick = () => {
  434. store.dispatch(actionNewOrder());
  435. }
  436. })
  437. store.subscribe(() => {
  438. dashboardUl.innerHTML = ''
  439. const {orders} = store.getState().promise;
  440. const [,route, _id] = location.hash.split('/');
  441. if(orders?.payload && route === 'dashboard'){
  442. for(let {createdAt, total, orderGoods} of orders.payload){
  443. let date = new Date(+createdAt);
  444. let li = document.createElement("li");
  445. for(let {count, good} of orderGoods){
  446. let div = document.createElement("div");
  447. div.innerHTML = `<strong>${good.name}</strong>
  448. <span>${count} &#10006; ${good.price}</span>
  449. `
  450. li.append(div);
  451. }
  452. li.innerHTML += `<div>${total}</div>
  453. <div>${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}</div>
  454. <hr>`
  455. dashboardUl.append(li)
  456. }
  457. }
  458. })
  459. logout.onclick = () => store.dispatch(actionAuthLogout() )