App.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  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. import {useEffect, useState} from 'react';
  7. import {Router, Route, Link, Redirect, match} from 'react-router-dom';
  8. import {createBrowserHistory} from 'history';
  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. } catch (error) {
  20. dispatch(actionRejected(name, error));
  21. }
  22. };
  23. const getGQL = url =>
  24. (query, variables = {}) =>
  25. fetch(url, {
  26. //метод
  27. method: 'POST',
  28. headers: {
  29. //заголовок content-type
  30. 'Content-Type': 'application/json',
  31. ...(localStorage.authToken ? {'Authorization': 'Bearer ' + localStorage.authToken} :
  32. {})
  33. },
  34. //body с ключами query и variables
  35. body: JSON.stringify({query, variables})
  36. })
  37. .then(res => res.json())
  38. .then(data => {
  39. if (data.errors && !data.data)
  40. throw new Error(JSON.stringify(data.errors));
  41. return data.data[Object.keys(data.data)[0]];
  42. });
  43. const backendURL = 'http://shop-roles.asmer.fs.a-level.com.ua';
  44. const gql = getGQL(backendURL + '/graphql');
  45. function jwtDecode(token) {
  46. try {
  47. let decoded = token.split('.');
  48. decoded = decoded[1];
  49. decoded = atob(decoded);
  50. decoded = JSON.parse(decoded);
  51. return decoded;
  52. } catch (e) {
  53. return;
  54. }
  55. }
  56. //скопировать
  57. //3 редьюсера
  58. //экшоны к ним
  59. //
  60. const actionRootCats = () =>
  61. actionPromise('rootCats', gql(`query {
  62. CategoryFind(query: "[{\\"parent\\":null}]"){
  63. _id name
  64. }
  65. }`));
  66. const actionCartAdd = (good, count = 1) => ({type: 'CART_ADD', good, count});
  67. const actionCartRemove = (good, count = 1) => ({type: 'CART_REMOVE', good, count});
  68. const actionCartChange = (good, count = 1) => ({type: 'CART_CHANGE', good, count});
  69. const actionCartClear = (good, count = 1) => ({type: 'CART_CLEAR', good, count});
  70. function cartReducer(state = {}, {type, good = {}, count = 1}) {
  71. const {_id} = good;
  72. const types = {
  73. CART_ADD() {
  74. return {
  75. ...state,
  76. [_id]: {good, count: count + (state[_id]?.count || 0)}
  77. };
  78. },
  79. CART_REMOVE() {
  80. let newState = {...state};
  81. delete newState[_id];
  82. return {
  83. ...newState
  84. };
  85. },
  86. CART_CHANGE() {
  87. return {
  88. ...state,
  89. [_id]: {good, count}
  90. };
  91. },
  92. CART_CLEAR() {
  93. return {};
  94. },
  95. };
  96. if (type in types)
  97. return types[type]();
  98. return state;
  99. }
  100. function authReducer(state, {type, token}) {
  101. if (!state) {
  102. if (localStorage.authToken) {
  103. type = 'AUTH_LOGIN';
  104. token = localStorage.authToken;
  105. } else {
  106. return {};
  107. }
  108. }
  109. if (type === 'AUTH_LOGIN') {
  110. let auth = jwtDecode(token);
  111. if (auth) {
  112. localStorage.authToken = token;
  113. return {token, payload: auth};
  114. }
  115. }
  116. if (type === 'AUTH_LOGOUT') {
  117. localStorage.authToken = '';
  118. return {};
  119. }
  120. return state;
  121. }
  122. function promiseReducer(state = {}, {type, name, status, payload, error}) {
  123. if (type === 'PROMISE') {
  124. return {
  125. ...state,
  126. [name]: {status, payload, error}
  127. };
  128. }
  129. return state;
  130. }
  131. // Actions =============================
  132. // Логин и логаут
  133. const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token});
  134. const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'});
  135. const actionLogin = (login = 'tst', password = '123') =>
  136. actionPromise('login', gql(`query ($login:String, $password:String){ login(login:$login, password:$password)}`, {
  137. 'login': login,
  138. 'password': password
  139. }));
  140. const actionFullLogin = (login = 'tst', password = '123') =>
  141. async dispatch => {
  142. let token = await dispatch(actionLogin(login, password));
  143. console.log(token);
  144. if (token) {
  145. dispatch(actionAuthLogin(token));
  146. }
  147. };
  148. // Регистрация
  149. const actionRegister = (login = 'tst', password = '123') =>
  150. actionPromise('login', gql(`mutation reg($login:String, $password:String) {
  151. UserUpsert(user:{login:$login, password:$password, nick:$login}){
  152. _id login
  153. }
  154. }`, {'login': login, 'password': password}));
  155. const actionFullRegister = (login = 'tst', password = '123') =>
  156. async dispatch => {
  157. console.log(login, password);
  158. await dispatch(actionRegister(login, password));
  159. await dispatch(actionFullLogin(login, password));
  160. };
  161. // Корневые категории
  162. // Товары категории
  163. const actionCatById = (_id) =>
  164. actionPromise('catById', gql(`query ($q: String){
  165. CategoryFindOne(query: $q){
  166. _id name goods {
  167. _id name price images {
  168. url
  169. }
  170. }
  171. subCategories {
  172. _id name
  173. }
  174. }
  175. }`, {q: JSON.stringify([{_id}])}));
  176. const actionGoodById = (_id) =>
  177. actionPromise('goodById', gql(`query ($good:String) {
  178. GoodFindOne(query:$good) {
  179. _id name price images {
  180. url
  181. }
  182. }
  183. }`, {good: JSON.stringify([{_id}])}));
  184. const store = createStore(combineReducers(
  185. {
  186. promise: promiseReducer,
  187. auth: authReducer,
  188. cart: cartReducer
  189. }),
  190. applyMiddleware(thunk));
  191. store.subscribe(() => console.log(store.getState()));
  192. store.dispatch(actionRootCats());
  193. const Logo = () =>
  194. <Link to="/">
  195. <img src={logo} className="Logo" alt="logo"/>
  196. </Link>;
  197. const Header = () =>
  198. <header className="header">
  199. <Logo/>
  200. <Login/>
  201. <CKoshik/>
  202. </header>;
  203. const CategoryListItem = ({_id, name}) =>
  204. <li>
  205. <Link to={`/category/${_id}`}>{name}</Link>
  206. </li>;
  207. const CategoryList = ({cats}) => {
  208. return (
  209. <ul>
  210. {cats.map((item) => <CategoryListItem key={item._id} {...item}/>)}
  211. </ul>
  212. );
  213. };
  214. const CCategoryList = connect(state => ({cats: state.promise.rootCats?.payload || []}))(CategoryList);
  215. const PageGood = ({good: {name, price, images}, match: {params: {_id}}, getData}) => {
  216. const isFirstImgExists = images && images[0] && images[0].url;
  217. useEffect(() => {
  218. getData(_id);
  219. }, []);
  220. return (
  221. <div>
  222. <h2>{name}</h2>
  223. {isFirstImgExists && <img src={`${backendURL}/${images[0].url}`} alt={name}/>}
  224. <strong>{price} грн</strong>
  225. </div>
  226. );
  227. };
  228. const CPageGood = connect(state => ({
  229. good: state.promise.goodById?.payload ?? [],
  230. }), {getData: actionGoodById})(PageGood);
  231. const Aside = () =>
  232. <aside className="aside">
  233. <CCategoryList/>
  234. </aside>;
  235. const GoodCard = ({good: {_id, name, price, images}, onAdd}) =>
  236. <li className="GoodCard">
  237. <h2>{name}</h2>
  238. {images && images[0] && images[0].url && <img src={`${backendURL}/${images[0].url}`}/>}
  239. <strong>{price} грн</strong>
  240. <button onClick={() => onAdd({_id, name, price, images})}>+</button>
  241. <Link to={`/good/${_id}`}>
  242. Подробнее
  243. </Link>
  244. </li>;
  245. const Login = () =>
  246. <Link to="/login">
  247. <h2 className="login">Личный кабинет</h2>
  248. </Link>;
  249. const CGoodCard = connect(null, {
  250. onAdd: actionCartAdd
  251. })(GoodCard);
  252. const Koshik = ({cart}) => {
  253. let goodsInCart = cart;
  254. let allGoodsInCart = 0;
  255. for (let key in goodsInCart) {
  256. allGoodsInCart += goodsInCart[key].count;
  257. }
  258. return (
  259. <h2 className="koshikCounter">
  260. <Link to="/cart">В корзине:{allGoodsInCart}
  261. </Link>
  262. </h2>
  263. );
  264. };
  265. const CKoshik = connect(({cart}) => ({cart}))(Koshik);
  266. const Category = ({cat: {name, goods = []} = {}}) => {
  267. return (
  268. <div className="Category">
  269. <h1>{name}</h1>
  270. <ul className="ul">
  271. {(goods || []).map(good => <CGoodCard key={good._id} good={good}/>)}
  272. </ul>
  273. </div>
  274. );
  275. };
  276. const CCategory = connect(state => ({cat: state.promise.catById?.payload || {}}))(Category);
  277. const Cart = ({cart, onCartChange, onCartRemove}) => {
  278. const error = typeof cart === 'undefined';
  279. return (
  280. <div className="Cart">
  281. <h1>Корзина</h1>
  282. <div className="containerForCart">
  283. <div className="wrapperForName">
  284. {error === false &&
  285. (cart.map((good) => {
  286. return (
  287. <div className="name" key={good.good.name}>{good.good.name}</div>
  288. );
  289. })
  290. )
  291. }
  292. </div>
  293. <div className="wrapperForCount">
  294. {error === false &&
  295. (cart.map((good) => {
  296. return <input
  297. className="count"
  298. type="number"
  299. defaultValue={good.count}
  300. onChange={(e) => {
  301. onCartChange(good.good, +e.target.value);
  302. }
  303. }
  304. key={good.good._id}
  305. />;
  306. })
  307. )}
  308. </div>
  309. <div className="wrapperForName">
  310. {error === false &&
  311. (cart.map((good) => {
  312. return (
  313. <button key={good.good._id} onClick={() => onCartRemove(good.good, good.count)
  314. }>
  315. Удалить {good.good.name}</button>);
  316. })
  317. )
  318. }
  319. </div>
  320. </div>
  321. </div>
  322. );
  323. };
  324. const CCart = connect(
  325. state => ({cart: Object.values(state.cart) || []}),
  326. {
  327. onCartChange: actionCartChange,
  328. onCartRemove: actionCartRemove
  329. })(Cart);
  330. const LoginForm = ({onCartLogin}) => {
  331. const [login, setLogin] = useState('');
  332. const [password, setPassword] = useState('');
  333. const isDisabled = login === '' || password === '';
  334. console.log(login, password, isDisabled);
  335. const onChange = (event) => {
  336. event.target.name === 'login' ? setLogin(event.target.value) : setPassword(event.target.value);
  337. };
  338. return (
  339. <div className="inputWrapper">
  340. <h2>Вход в личный кабинет</h2>
  341. <input
  342. className="inputLogin"
  343. name="login"
  344. style={{borderColor: isDisabled ? 'red' : ''}}
  345. placeholder="введите логин"
  346. type="text"
  347. onChange={onChange}
  348. />
  349. <input
  350. className="inputPsw"
  351. name="password"
  352. style={{borderColor: isDisabled ? 'red' : ''}}
  353. placeholder="введите пароль"
  354. type="password"
  355. onChange={onChange}
  356. />
  357. <button
  358. className="buttonLogin"
  359. disabled={isDisabled}
  360. onClick={
  361. () => {
  362. onCartLogin(login, password);
  363. }}>Login
  364. </button>
  365. <h4>Вы еще не зарегистрированы?</h4>
  366. <Link to="/registration">Зарегистрироваться</Link>
  367. </div>
  368. );
  369. };
  370. const CLoginForm = connect(
  371. null,
  372. {
  373. onCartLogin: actionFullLogin
  374. })(LoginForm);
  375. const RegisterForm = ({onCartRegister}) => {
  376. const [login, setLogin] = useState('');
  377. const [password, setPassword] = useState('');
  378. const isDisabled = login === '' || password === '';
  379. console.log(login, password, isDisabled);
  380. const onChange = (event) => {
  381. event.target.name === 'login' ? setLogin(event.target.value) : setPassword(event.target.value);
  382. };
  383. return (
  384. <div className="inputWrapper">
  385. <h2>Регистрация</h2>
  386. <input
  387. className="inputLogin"
  388. name="userName"
  389. style={{borderColor: isDisabled ? 'red' : ''}}
  390. placeholder="введите имя"
  391. type="text"
  392. onChange={onChange}
  393. />
  394. <input
  395. className="inputLogin"
  396. name="login"
  397. style={{borderColor: isDisabled ? 'red' : ''}}
  398. placeholder="*введите логин"
  399. type="text"
  400. onChange={onChange}
  401. />
  402. <input
  403. className="inputPsw"
  404. name="password"
  405. style={{borderColor: isDisabled ? 'red' : ''}}
  406. placeholder="*введите пароль"
  407. type="password"
  408. onChange={onChange}
  409. />
  410. <button
  411. className="buttonLogin"
  412. disabled={isDisabled}
  413. onClick={
  414. () => {
  415. onCartRegister(login, password);
  416. }}>Login
  417. </button>
  418. <Link to="/login">Войти в личный кабинет</Link>
  419. </div>
  420. );
  421. };
  422. const CRegisterForm = connect(
  423. null,
  424. {
  425. onCartRegister: actionFullRegister
  426. })(RegisterForm);
  427. const PageMain = () =>
  428. <h1>Главная страничка</h1>;
  429. const PageCategory = ({match: {params: {_id}}, getData}) => {
  430. useEffect(() => {
  431. getData(_id);
  432. }, [_id]);
  433. return (
  434. <div>
  435. <CCategory/>
  436. </div>
  437. );
  438. };
  439. const CPageCategory = connect(null, {getData: actionCatById})(PageCategory);
  440. const Main = () =>
  441. <main className="main">
  442. <Aside/>
  443. <Content>
  444. <Route path="/" component={PageMain} exact/>
  445. <Route path="/category/:_id" component={CPageCategory} exact/>
  446. <Route path="/cart" component={CCart} exact/>
  447. <Route path="/login" component={CLoginForm} exact/>
  448. <Route path="/registration" component={CRegisterForm} exact/>
  449. <Route path="/good/:_id" component={CPageGood} exact/>
  450. {/*<CCategory/>*/}
  451. </Content>
  452. </main>;
  453. const Content = ({children}) =>
  454. <div className="Content">
  455. {children}
  456. </div>;
  457. const Footer = () =>
  458. <footer className="Footer">
  459. <Logo/>
  460. </footer>;
  461. const history = createBrowserHistory();
  462. //import {Provider, connect} from 'react-redux';
  463. function App() {
  464. return (
  465. <Router history={history}>
  466. <Provider store={store}>
  467. <div className="App">
  468. <Header/>
  469. <Main/>
  470. <Footer/>
  471. </div>
  472. </Provider>
  473. </Router>
  474. );
  475. }
  476. export default App;