App.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. import logo from './logo.svg';
  2. import './App.scss';
  3. import thunk from 'redux-thunk';
  4. import {createStore, combineReducers, applyMiddleware} from 'redux';
  5. import {Provider, connect} from 'react-redux';
  6. const actionPending = name => ({type: 'PROMISE', status: 'PENDING', name})
  7. const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
  8. const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
  9. const actionPromise = (name, promise) =>
  10. async dispatch => {
  11. dispatch(actionPending(name)) // 1. {delay1000: {status: 'PENDING'}}
  12. try{
  13. let payload = await promise
  14. dispatch(actionResolved(name, payload))
  15. return payload
  16. }
  17. catch(error){
  18. dispatch(actionRejected(name, error))
  19. }
  20. }
  21. const getGQL = url =>
  22. (query, variables = {}) =>
  23. fetch(url, {
  24. //метод
  25. method: 'POST',
  26. headers: {
  27. //заголовок content-type
  28. "Content-Type": "application/json",
  29. ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } :
  30. {})
  31. },
  32. //body с ключами query и variables
  33. body: JSON.stringify({ query, variables })
  34. })
  35. .then(res => res.json())
  36. .then(data => {
  37. if (data.errors && !data.data)
  38. throw new Error(JSON.stringify(data.errors))
  39. return data.data[Object.keys(data.data)[0]]
  40. })
  41. const backendURL = "http://shop-roles.asmer.fs.a-level.com.ua"
  42. const gql = getGQL(backendURL + '/graphql')
  43. function jwtDecode(token) {
  44. try {
  45. let decoded = token.split('.')
  46. decoded = decoded[1]
  47. decoded = atob(decoded)
  48. decoded = JSON.parse(decoded)
  49. return decoded
  50. } catch (e) {
  51. return;
  52. }
  53. }
  54. const actionRootCats = () =>
  55. actionPromise('rootCats', gql(`query {
  56. CategoryFind(query: "[{\\"parent\\":null}]"){
  57. _id name
  58. }
  59. }`))
  60. const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count })
  61. const actionCartRemove = (good, count = 1) => ({ type: 'CART_REMOVE', good, count })
  62. const actionCartChange = (good, count = 1) => ({ type: 'CART_CHANGE', good, count })
  63. const actionCartClear = (good, count = 1) => ({ type: 'CART_CLEAR', good, count })
  64. function cartReducer(state = {}, { type, good={}, count=1}){
  65. //{
  66. // _id1: {good, count}
  67. // _id2: {good, count}
  68. //}
  69. const {_id } = good
  70. const types = {
  71. CART_ADD() {
  72. //как CHANGE, только если ключ раньше был, то достать из него count и добавить
  73. //к count из action. Если не было, достать 0 и добавить к count из action
  74. return {
  75. ...state,
  76. [_id]: { good, count: count + (state[_id]?.count || 0)}
  77. }
  78. },
  79. CART_REMOVE(){ //смочь скопировать объект и выкинуть ключ. как вариант через
  80. //деструктуризацию
  81. let newState = { ...state }
  82. delete newState[_id]
  83. return {
  84. ...newState
  85. }
  86. },
  87. CART_CHANGE(){
  88. return {
  89. ...state, //по аналогии с promiseReducer дописать
  90. [_id]:{good, count}
  91. }
  92. },
  93. CART_CLEAR(){
  94. return {}
  95. },
  96. }
  97. if (type in types)
  98. return types[type]()
  99. return state
  100. }
  101. function authReducer(state, {type, token}){
  102. if (!state) {
  103. if (localStorage.authToken) {
  104. type = 'AUTH_LOGIN'
  105. token = localStorage.authToken
  106. } else {
  107. return {}
  108. }
  109. //проверить localStorage.authToken на наличие
  110. //если есть - сделать так, что бы следующий if сработал
  111. //если нет - вернуть {}
  112. }
  113. if (type === 'AUTH_LOGIN') {
  114. let auth = jwtDecode(token)
  115. if (auth) {
  116. localStorage.authToken = token
  117. return { token, payload: auth }
  118. }
  119. //взять токен из action
  120. //попытаться его jwtDecode
  121. //если удалось, то:
  122. //сохранить токен в localStorage
  123. //вернуть объект вида {токен, payload: раскодированный токен}
  124. }
  125. if (type === 'AUTH_LOGOUT') {
  126. localStorage.authToken = ''
  127. return {}
  128. //почистить localStorage
  129. //вернуть пустой объект
  130. }
  131. return state
  132. }
  133. function promiseReducer(state={}, {type, name, status, payload, error}){
  134. //{
  135. // login: {status, payload, error}
  136. // catById: {status, payload, error}
  137. //}
  138. if (type === 'PROMISE'){
  139. return {
  140. ...state,
  141. [name]:{status, payload, error}
  142. }
  143. }
  144. return state
  145. }
  146. // Actions =============================
  147. // Логин и логаут
  148. const actionAuthLogin = (token) => ({ type: 'AUTH_LOGIN', token })
  149. const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' })
  150. const actionLogin = (login = 'tst', password = '123') =>
  151. actionPromise('login', gql(`query ($login:String, $password:String){ login(login:$login, password:$password)}`, { 'login': login, 'password': password }))
  152. const actionFullLogin = (login = 'tst', password = '123') =>
  153. async dispatch => {
  154. let token = await dispatch(actionLogin(login, password))
  155. if (token) {
  156. dispatch(actionAuthLogin(token))
  157. }
  158. }
  159. // Регистрация
  160. const actionRegister = (login = 'tst', password = '123') =>
  161. actionPromise('login', gql(`mutation reg($login:String, $password:String) {
  162. UserUpsert(user:{login:$login, password:$password, nick:$login}){
  163. _id login
  164. }
  165. }`, { 'login': login, 'password': password }))
  166. const actionFullRegister = (login = 'tst', password = '123') =>
  167. async dispatch => {
  168. await dispatch(actionRegister(login, password))
  169. await dispatch(actionFullLogin(login, password))
  170. }
  171. // Корневые категории
  172. // Товары категории
  173. const actionCatById = (_id) =>
  174. actionPromise('catById', gql(`query ($q: String){
  175. CategoryFindOne(query: $q){
  176. _id name goods {
  177. _id name price images {
  178. url
  179. }
  180. }
  181. subCategories {
  182. _id name
  183. }
  184. }
  185. }`, { q: JSON.stringify([{ _id }]) }))
  186. const actionGoodById = (_id) =>
  187. actionPromise('goodById', gql(`query ($good:String) {
  188. GoodFindOne(query:$good) {
  189. _id name price images {
  190. url
  191. }
  192. }
  193. }`, { good: JSON.stringify([{ _id }]) }))
  194. const store = createStore(combineReducers({promise: promiseReducer,
  195. auth: authReducer,
  196. cart: cartReducer}), applyMiddleware(thunk))
  197. store.subscribe(() => console.log(store.getState()))
  198. store.dispatch(actionRootCats())
  199. store.dispatch(actionCatById('5dc458985df9d670df48cc47'))
  200. const Logo = () =>
  201. <img src={logo} className="Logo" alt="logo" />
  202. const Header = () =>
  203. <header>
  204. <Logo />
  205. <CKoshik />
  206. </header>
  207. const CategoryListItem = ({_id, name}) =>
  208. <li><a href={`#/category/${_id}`}>{name}</a></li>
  209. const CategoryList = ({cats}) =>
  210. <ul>
  211. {cats.map((item) => <CategoryListItem {...item} />)}
  212. </ul>
  213. const CCategoryList = connect(state => ({cats:state.promise.rootCats?.payload || []}))
  214. (CategoryList)
  215. const Aside = () =>
  216. <aside className='aside'>
  217. <CCategoryList />
  218. </aside>
  219. const GoodCard = ({good:{_id, name, price, images}, onAdd}) =>
  220. <li className='GoodCard'>
  221. <h2>{name}</h2>
  222. {images && images[0] && images[0].url && <img src={backendURL + '/' + images[0].url} />}
  223. <strong>{price}</strong>
  224. <button onClick={() => onAdd({_id, name, price, images})}>+</button>
  225. </li>
  226. const CGoodCard = connect(null, {onAdd: actionCartAdd})(GoodCard)
  227. const Koshik = ({cart}) => {
  228. let goodsInCart = cart
  229. let allGoodsInCart = 0
  230. for (let key in goodsInCart) {
  231. allGoodsInCart += goodsInCart[key].count
  232. }
  233. return (
  234. <h2>{allGoodsInCart}</h2>
  235. )
  236. }
  237. const CKoshik = connect(({cart}) => ({cart}))(Koshik)
  238. const Category = ({cat:{name, goods=[]}={}}) =>
  239. <div className='Category'>
  240. <h1>{name}</h1>
  241. <ul>
  242. {goods.map(good => <CGoodCard good={good} />)}
  243. </ul>
  244. </div>
  245. const CCategory = connect(state => ({cat:state.promise.catById?.payload || {}}))
  246. (Category)
  247. // {` нарисовать страницу корзины , по изменению в input <input onChange={() => onCartChange(...)}
  248. // по кнопке удалить <button onClick={() => onCartRemove(....)}`}
  249. // ТУТ БУДЕТ КОРЗИНА
  250. const Cart = ({cart = {}, onCartChange, onCartRemove}) =>{
  251. let goodsInCart = Object.values(cart)
  252. return (
  253. <div> {console.log('cart',cart), console.log('',)}
  254. <h1> КОРЗИНА</h1>
  255. <ul>
  256. {goodsInCart.map(item => {
  257. let {good, good: {_id, name, price, images: [{url}]}, count} = item;
  258. return (
  259. <li key = { _id}>
  260. <h2>{name}</h2>
  261. <img src={backendURL + '/' + url} />
  262. <p>Количество{count}</p>
  263. <strong>{price}$</strong>
  264. <input type="number" min="1" onChange = {(e) => onCartChange(good, e.target.value)}></input>
  265. <button onClick={() => onCartRemove(good,count)}>Удалить</button>
  266. </li>)
  267. } )}
  268. </ul>
  269. </div>
  270. )
  271. }
  272. //const CCart = connect(забрать из редакса корзину положить в пропс cart,
  273. //дать компоненту onCartChange и onCartRemove с соответствующими actionCreator)(Cart)
  274. const CCart = connect(state => (
  275. {cart: state.cart }),
  276. {onCartChange: actionCartChange,
  277. onCartRemove:actionCartRemove})(Cart)
  278. const Main = () =>
  279. <main>
  280. <Aside />
  281. <Content>
  282. <Category cat={{name: 'ЗАГЛУШКА'}} />
  283. <CCategory />
  284. <CCart/>
  285. </Content>
  286. </main>
  287. const Content = ({children}) =>
  288. <div className="Content">
  289. {children}
  290. </div>
  291. const Footer = () =>
  292. <footer>
  293. <Logo />
  294. </footer>
  295. function App() {
  296. return (
  297. <Provider store={store}>
  298. <div className="App">
  299. <Header />
  300. <Main />
  301. <Footer />
  302. </div>
  303. </Provider>
  304. );
  305. }
  306. export default App;