App.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import { useEffect, useState } from 'react';
  2. import logo from './logo.svg';
  3. import './App.scss';
  4. import thunk from 'redux-thunk';
  5. import {createStore, combineReducers, applyMiddleware} from 'redux';
  6. import {Provider, connect} from 'react-redux';
  7. import { Link, Route, Router, Switch, Redirect } from 'react-router-dom';
  8. import createHistory from 'history/createBrowserHistory'
  9. const actionPending = name => ({type: 'PROMISE', status: 'PENDING', name})
  10. const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
  11. const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
  12. const actionPromise = (name, promise) =>
  13. async dispatch => {
  14. dispatch(actionPending(name)) // 1. {delay1000: {status: 'PENDING'}}
  15. try{
  16. let payload = await promise
  17. dispatch(actionResolved(name, payload))
  18. return payload
  19. }
  20. catch(error){
  21. dispatch(actionRejected(name, error))
  22. }
  23. }
  24. const getGQL = url =>
  25. (query, variables = {}) =>
  26. fetch(url, {
  27. method: 'POST',
  28. headers: {
  29. "Content-Type": "application/json",
  30. ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } :
  31. {})
  32. },
  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. const {_id } = good
  66. const types = {
  67. CART_ADD() {
  68. return {
  69. ...state,
  70. [_id]: { good, count: count + (state[_id]?.count || 0)}
  71. }
  72. },
  73. CART_REMOVE(){
  74. let newState = { ...state }
  75. delete newState[_id]
  76. return {
  77. ...newState
  78. }
  79. },
  80. CART_CHANGE(){
  81. return {
  82. ...state,
  83. [_id]:{good, count}
  84. }
  85. },
  86. CART_CLEAR(){
  87. return {}
  88. },
  89. }
  90. if (type in types)
  91. return types[type]()
  92. return state
  93. }
  94. function authReducer(state, {type, token}){
  95. if (!state) {
  96. if (localStorage.authToken) {
  97. type = 'AUTH_LOGIN'
  98. token = localStorage.authToken
  99. } else {
  100. return {}
  101. }
  102. }
  103. if (type === 'AUTH_LOGIN') {
  104. let auth = jwtDecode(token)
  105. if (auth) {
  106. localStorage.authToken = token
  107. return { token, payload: auth }
  108. }
  109. }
  110. if (type === 'AUTH_LOGOUT') {
  111. localStorage.authToken = ''
  112. return {}
  113. }
  114. return state
  115. }
  116. function promiseReducer(state={}, {type, name, status, payload, error}){
  117. if (type === 'PROMISE'){
  118. return {
  119. ...state,
  120. [name]:{status, payload, error}
  121. }
  122. }
  123. return state
  124. }
  125. const actionAuthLogin = (token) => ({ type: 'AUTH_LOGIN', token })
  126. const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' })
  127. const actionLogin = (login = 'tst', password = '123') =>
  128. actionPromise('login', gql(`query ($login:String, $password:String){ login(login:$login, password:$password)}`, { 'login': login, 'password': password }))
  129. const actionFullLogin = (login = 'tst', password = '123') =>
  130. async dispatch => {
  131. let token = await dispatch(actionLogin(login, password))
  132. if (token) {
  133. dispatch(actionAuthLogin(token))
  134. }
  135. }
  136. const actionRegister = (login = 'tst', password = '123') =>
  137. actionPromise('login', gql(`mutation reg($login:String, $password:String) {
  138. UserUpsert(user:{login:$login, password:$password, nick:$login}){
  139. _id login
  140. }
  141. }`, { 'login': login, 'password': password }))
  142. const actionFullRegister = (login = 'tst', password = '123') =>
  143. async dispatch => {
  144. await dispatch(actionRegister(login, password))
  145. await dispatch(actionFullLogin(login, password))
  146. }
  147. const actionCatById = (_id) =>
  148. actionPromise('catById', gql(`query ($q: String){
  149. CategoryFindOne(query: $q){
  150. _id name goods {
  151. _id name price images {
  152. url
  153. }
  154. }
  155. subCategories {
  156. _id name
  157. }
  158. }
  159. }`, { q: JSON.stringify([{ _id }]) }))
  160. const actionGoodById = (_id) =>
  161. actionPromise('goodById', gql(`query ($good:String) {
  162. GoodFindOne(query:$good) {
  163. _id name price images {
  164. url
  165. }
  166. }
  167. }`, { good: JSON.stringify([{ _id }]) }))
  168. const store = createStore(combineReducers({promise: promiseReducer,
  169. auth: authReducer,
  170. cart: cartReducer}), applyMiddleware(thunk))
  171. store.subscribe(() => console.log(store.getState()))
  172. store.dispatch(actionRootCats())
  173. //store.dispatch(actionCatById('5dc458985df9d670df48cc47'))
  174. const Logo = () =>
  175. <Link to="/">
  176. <img src={logo} className="Logo" alt="logo" />
  177. </Link>
  178. const Header = () =>
  179. <header>
  180. <Logo />
  181. </header>
  182. const Navbar = () =>
  183. <nav className='Navbar'>
  184. <CKoshik/>
  185. <LogBtn />
  186. </nav>
  187. const CategoryListItem = ({_id, name}) =>
  188. <li className='CatLink'>
  189. <Link to={`/category/:${_id}`}>{name}</Link>
  190. </li>
  191. const CategoryList = ({cats}) =>
  192. <ul>{cats.map((item) => <CategoryListItem {...item}/>)}</ul>
  193. const CCategoryList = connect(state => ({cats:state.promise.rootCats?.payload || []}))(CategoryList)
  194. const Aside = () =>
  195. <aside><CCategoryList /></aside>
  196. const GoodCard = ({good:{_id, name, price, images}, onAdd}) =>
  197. <li className='GoodCard'>
  198. <h2>{name}</h2>
  199. {images && images[0] && images[0].url && <img className='GoodImg' alt='img' src={backendURL + '/' + images[0].url} />}
  200. <br/>
  201. <button>На страницу товара</button>
  202. <br/>
  203. <strong>Цена: {price}</strong>
  204. <br/>
  205. <button onClick={() => onAdd({_id, name, price, images})}>Добавить в корзину</button>
  206. </li>
  207. const CGoodCard = connect(null, {onAdd: actionCartAdd})(GoodCard)
  208. const LogBtn = () =>
  209. <div className='KoshikCnt item'>
  210. <Link to="/login" style={{color:'white', textDecoration:'none'}}>Войти</Link>
  211. </div>
  212. const Koshik = ({cart}) => {
  213. let goodsInCart = cart
  214. let allGoodsInCart = 0
  215. for (let key in goodsInCart) {
  216. allGoodsInCart += goodsInCart[key].count
  217. }
  218. return (
  219. <div className='KoshikCnt item'>
  220. <Link to="/cart" style={{color:'white', textDecoration:'none'}}>Корзина: {allGoodsInCart}</Link>
  221. </div>
  222. )
  223. }
  224. const CKoshik = connect(({cart}) => ({cart}))(Koshik)
  225. const Category = ({cat:{name, goods=[]}={}}) =>
  226. <div className='Category'>
  227. <h1>{name}</h1>
  228. <ul>
  229. {(goods || []).map(good => <CGoodCard good={good} />)}
  230. </ul>
  231. </div>
  232. const CCategory = connect(state => ({cat:state.promise.catById?.payload || {}}))
  233. (Category)
  234. const CartItem = ({cart:{_id, name, price, images}, count: {count}, onChange, onRemove}) => {
  235. console.log('good', _id)
  236. return(
  237. <li className='GoodCard'>
  238. <h2>{name}</h2>
  239. {images && images[0] && images[0].url && <img className='GoodImg' alt='img' src={backendURL + '/' + images[0].url} />}
  240. <br/>
  241. <strong>Цена: {price * count}</strong>
  242. <br/>
  243. <label>Кол-во покупки: <input type="number" value={count} min="1" onInput={(e) => onChange({_id, name, price, images}, e.target.value)}/></label>
  244. <br/>
  245. <button>Заказать</button>
  246. <button onClick={() => onRemove({_id, name, price, images})}>Удалить заказ[X]</button>
  247. </li>
  248. )
  249. }
  250. const CCartItem = connect(null, {onChange: actionCartChange, onRemove: actionCartRemove})(CartItem)
  251. const Cart = ({cart}) => {
  252. let cartArr = []
  253. for(let item in cart) {
  254. cartArr.push(cart[item])
  255. }
  256. console.log('cartarr',cartArr)
  257. return(
  258. <div>
  259. <h1 style={{marginLeft:'30px'}}>Корзина</h1>
  260. <ul>{cartArr.map(item => <CCartItem cart={item.good} count={item} />)}</ul>
  261. </div>
  262. )
  263. }
  264. const CCart = connect(state => ({cart:state.cart}))(Cart)
  265. const PageCart = ({match: {params: {_id}}, getData}) => {
  266. useEffect(() => {
  267. getData(_id.substring(1))
  268. console.log('get', _id,typeof _id)
  269. },[_id])
  270. return (
  271. <CCart />
  272. )
  273. }
  274. const CPageCart= connect(null, {getData: actionCatById})(PageCart)
  275. //const CCart = connect(забрать из редакса корзину положить в пропс cart,
  276. //дать компоненту onCartChange и onCartRemove с соответствующими actionCreator)(Cart)
  277. const LoginForm = ({onLogin}) => {
  278. let [pass, setPass] = useState()
  279. let [login, setLogin] = useState()
  280. return (
  281. <div className='form'>
  282. <h3>Login Form</h3>
  283. <input placeholder='login' style={{outlineColor: login? 'black' : 'firebrick'}} onChange={(e) => setLogin(e.target.value)}/>
  284. <br/>
  285. <input placeholder='password' type="password" style={{outlineColor: pass? 'black' : 'firebrick'}} onChange={(e) => setPass(e.target.value)}/>
  286. <br/>
  287. <button disabled={!pass || !login} onClick={() => onLogin(login, pass)}>Login</button>
  288. </div>
  289. )
  290. }
  291. const CLoginForm = connect(null, {onLogin: actionFullLogin})(LoginForm)
  292. const PageMain = () => <h1>MAIN PAGE</h1>
  293. const PageCategory = ({match: {params: {_id}}, getData}) => {
  294. useEffect(() => {
  295. getData(_id.substring(1))
  296. console.log('get', _id,typeof _id)
  297. },[_id])
  298. return (
  299. <CCategory />
  300. )
  301. }
  302. const CPageCategory = connect(null, {getData: actionCatById})(PageCategory)
  303. const Page404 = () => <h1> 404 </h1>
  304. const Main = () =>
  305. <main>
  306. <Aside />
  307. <Content>
  308. <Switch>
  309. <Redirect from='/main' to='/' />
  310. <Route path="/" component={PageMain} exact/>
  311. <Route path="/category/:_id" component={CPageCategory}/>
  312. <Route path="/cart" component={CCart} />
  313. <Route path="*" component={Page404} />
  314. {/* <CCategory /> */}
  315. {/* <LoginForm onLogin={(l, p) => actionFullLogin(l, p)}/> */}
  316. {/* <CLoginForm /> */}
  317. {/* <CCart /> */}
  318. </Switch>
  319. </Content>
  320. </main>
  321. const Content = ({children}) =>
  322. <div className="Content">{children}</div>
  323. const Footer = () =>
  324. <footer><Logo /></footer>
  325. const history = createHistory()
  326. function App() {
  327. return (
  328. <Router history={history}>
  329. <Provider store={store}>
  330. <div className="App">
  331. <Header />
  332. <Navbar />
  333. <Main />
  334. <Footer />
  335. {/* <CCart /> */}
  336. </div>
  337. </Provider>
  338. </Router>
  339. );
  340. }
  341. export default App;