App.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import "./App.scss";
  2. import {createStore, combineReducers, applyMiddleware} from "redux";
  3. import {Provider, connect} from "react-redux";
  4. import thunk from "redux-thunk";
  5. import React, {useState, useEffect} from "react";
  6. import {Router, Route, Link, Redirect, Switch} from 'react-router-dom';
  7. import createHistory from "history/createBrowserHistory";
  8. const promiseReducer = function(state={}, {type, name, status, payload, error}) {
  9. if (type == 'PROMISE'){
  10. return {
  11. ...state,
  12. [name]:{status, payload, error}
  13. }
  14. }
  15. return state;
  16. };
  17. const actionPending = name => ({type: "PROMISE", name, status: 'PENDING'});
  18. const actionFulfilled = (name,payload) => ({type: "PROMISE", name, status: 'FULFILLED', payload});
  19. const actionRejected = (name,error) => ({type: "PROMISE", name, status: 'REJECTED', error});
  20. const actionPromise = function(name, promise) {
  21. return async dispatch => {
  22. dispatch(actionPending(name));
  23. try {
  24. let payload = await promise
  25. dispatch(actionFulfilled(name, payload))
  26. return payload
  27. }
  28. catch(error){
  29. dispatch(actionRejected(name, error))
  30. };
  31. };
  32. };
  33. let jwtDecode = function(token) {
  34. let payloadInBase64;
  35. let payloadInJson;
  36. let payload;
  37. try {
  38. payloadInBase64 = token.split(".")[1];
  39. payloadInJson = atob(payloadInBase64);
  40. payload = JSON.parse(payloadInJson);
  41. return payload;
  42. }
  43. catch(err) {
  44. }
  45. };
  46. const authReducer = function(state, {type, token}) {
  47. let payload;
  48. if (state == undefined) {
  49. if(localStorage.authToken) {
  50. type = "AUTH_LOGIN";
  51. token = localStorage.authToken;
  52. } else {
  53. type = "AUTH_LOGOUT";
  54. };
  55. };
  56. if (type == "AUTH_LOGIN") {
  57. payload = jwtDecode(token);
  58. if(payload) {
  59. localStorage.authToken = token;
  60. return {
  61. token: token,
  62. payload: payload
  63. }
  64. }
  65. };
  66. if (type == "AUTH_LOGOUT") {
  67. localStorage.removeItem("authToken");
  68. return {};
  69. };
  70. return state || {};
  71. };
  72. const actionAuthLogin = token => ({type: "AUTH_LOGIN", token});
  73. const actionAuthLogout = () => ({type: "AUTH_LOGOUT"});
  74. const actionFullLogin = function(login, password) {
  75. return async dispatch => {
  76. let token = await gql("query userLogin($login: String, $password: String) {login(login: $login, password: $password)}", {"login": login, "password": password});
  77. dispatch(actionAuthLogin(token));
  78. };
  79. };
  80. let actionFullRegister = function(login, password, nick) {
  81. return async dispatch => {
  82. dispatch(actionPromise("userRegister", gql(`mutation userRegister($login:String, $password:String, $nick:String) {
  83. UserUpsert(user: {login:$login, password:$password, nick:$nick}) {
  84. _id login nick
  85. }
  86. }`,
  87. {
  88. "login": login,
  89. "password": password,
  90. "nick": nick
  91. })));
  92. dispatch(actionFullLogin(login, password));
  93. };
  94. };
  95. let cartReducer = function(state={}, {type, good, count=1}) {
  96. if(type == "CART_ADD") {
  97. let newState = {...state};
  98. if(good["_id"] in state) {
  99. newState[good._id] = {count: newState[good._id].count + count, good}
  100. }
  101. else {
  102. newState = {
  103. ...state,
  104. [good._id]: {count, good}
  105. };
  106. };
  107. return newState;
  108. };
  109. if(type == "CART_CHANGE") {
  110. let newState = {...state,
  111. [good._id]: {count, good}
  112. };
  113. return newState;
  114. };
  115. if(type == "CART_DELETE") {
  116. let newState = {...state};
  117. delete newState[good._id];
  118. return newState;
  119. };
  120. if(type == "CART_CLEAR") {
  121. return {};
  122. };
  123. return state;
  124. };
  125. const actionCartAdd = (good, count=1) => ({type: 'CART_ADD', good, count: +count})
  126. const actionCartChange = (good, count=1) => ({type: 'CART_CHANGE', good, count: +count})
  127. const actionCartDelete = (good) => ({type: 'CART_DELETE', good})
  128. const actionCartClear = () => ({type: 'CART_CLEAR'})
  129. const getGQL = function(url) {
  130. return async function(query, variables) {
  131. const res = await fetch(url, {
  132. method: "POST",
  133. headers: {
  134. "Content-Type": "application/json",
  135. ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } : {})
  136. },
  137. body: JSON.stringify({ query, variables })
  138. });
  139. const data = await res.json();
  140. if (data.data) {
  141. return Object.values(data.data)[0];
  142. }
  143. else {
  144. throw new Error(JSON.stringify(data.errors));
  145. };
  146. };
  147. };
  148. const backendURL = 'http://shop-roles.asmer.fs.a-level.com.ua';
  149. const gql = getGQL(backendURL + '/graphql');
  150. let actionRootCats = function() {
  151. return actionPromise("rootCats", gql(`query {
  152. CategoryFind(query: "[{\\"parent\\":null}]"){
  153. _id name
  154. }
  155. }`));
  156. };
  157. let actionCatById = function(_id) {
  158. return actionPromise("catById", gql(`query catById($q: String){
  159. CategoryFindOne(query: $q){
  160. _id name goods {
  161. _id name price images {
  162. url
  163. }
  164. }
  165. }
  166. }`,
  167. {q: JSON.stringify([{_id}])}
  168. ));
  169. };
  170. let actionGoodById = function(_id) {
  171. return actionPromise("goodById", gql(`query findGood($goodQuery: String) {
  172. GoodFindOne(query:$goodQuery) {
  173. _id name price images {
  174. url
  175. }
  176. }
  177. }`,
  178. {goodQuery: JSON.stringify([{"_id": _id}])}
  179. ));
  180. };
  181. let actionOrders = async function() {
  182. let order = await gql(`mutation makeOrder($order:OrderInput){
  183. OrderUpsert(order: $order){
  184. _id
  185. }
  186. }`, {
  187. "order": {
  188. orderGoods: Object.entries(store.getState().cart).map(([_id, count]) =>({"count": count.count, "good": {_id}}))
  189. }
  190. });
  191. store.dispatch(actionCartClear());
  192. }
  193. let actionOrdersFind = function() {
  194. return actionPromise("ordersFind", gql(`query ordersFind($query:String) {
  195. OrderFind(query: $query) {
  196. createdAt orderGoods {
  197. count good {
  198. name price images {
  199. url
  200. }
  201. }
  202. }
  203. }
  204. }`,
  205. {
  206. "query": JSON.stringify([{}])
  207. }));
  208. }
  209. const store = createStore(combineReducers({promise: promiseReducer,
  210. auth: authReducer,
  211. cart: cartReducer}), applyMiddleware(thunk));
  212. store.subscribe(() => console.log(store.getState()));
  213. store.dispatch(actionRootCats());
  214. let Nav = function({auth}) {
  215. return (
  216. <ul className="nav">
  217. <li>
  218. <Link to="/login">Логин</Link>
  219. </li>
  220. <li>
  221. <Link to="/registration">Регистрация</Link>
  222. </li>
  223. <li>
  224. <Link to="/cart">Корзина</Link>
  225. </li>
  226. <li>
  227. <Link to="/dashboard">История покупок</Link>
  228. </li>
  229. </ul>
  230. );
  231. };
  232. const CNav = connect(state => ({auth: state.auth}))(Nav);
  233. let Header = function() {
  234. return (
  235. <header className="header">
  236. <div className="header__logo">Типо логотип</div>
  237. <CNav />
  238. </header>
  239. );
  240. };
  241. let RootCategories = function({rootCats}) {
  242. return (
  243. <aside className="rootCats">
  244. <ul className="rootCats__list">
  245. {rootCats.map(rootCat => <li><Link to={`/category/${rootCat._id}`}>{rootCat.name}</Link></li>)}
  246. </ul>
  247. </aside>
  248. );
  249. };
  250. const CRootCategories = connect(state => ({rootCats: state.promise?.rootCats?.payload || []}))(RootCategories);
  251. let MainPage = function() {
  252. return (
  253. <h1>Главная страничка</h1>
  254. );
  255. };
  256. let GoodCard = function({good}) {
  257. return (
  258. <li className="goods__item">
  259. <img src={`${backendURL}/${good.images[0].url}`} />
  260. <b>{good.name}</b><br />
  261. <span>Цена: {good.price}</span><br />
  262. <Link to={`/good/${good._id}`}>Перейти на страничку товара</Link>
  263. </li>
  264. );
  265. };
  266. let Goods = function({goods}) {
  267. return (
  268. <section className="goods">
  269. <ul className="goods__list">
  270. {goods.map(good => <GoodCard good={good} />)}
  271. </ul>
  272. </section>
  273. );
  274. };
  275. const CGoods = connect(state => ({goods: state.promise?.catById?.payload?.goods || []}))(Goods);
  276. let Categories = function({match: {params: {_id}}, catById}) {
  277. catById(_id);
  278. return (
  279. <CGoods />
  280. );
  281. };
  282. const CCategories = connect(null, {catById: actionCatById})(Categories);
  283. let GoodPageCard = function({good, cartAdd}) {
  284. return (
  285. <section className="good">
  286. <img src={`${backendURL}/${good?.images[0]?.url}`} />
  287. <div>
  288. <b>{good?.name}</b><br />
  289. <span>Цена: {good?.price}</span><br />
  290. <button onClick={() => cartAdd(good)}>Добавить в корзину</button>
  291. </div>
  292. </section>
  293. );
  294. };
  295. const CGoodPageCard = connect(state => ({good: state.promise?.goodById?.payload}),
  296. {cartAdd: actionCartAdd})(GoodPageCard)
  297. let GoodPage = function({match: {params: {_id}}, goodById}) {
  298. goodById(_id);
  299. return (
  300. <CGoodPageCard />
  301. );
  302. };
  303. const CGoodPage = connect(null, {goodById: actionGoodById})(GoodPage);
  304. let Login = function({fullLogin}) {
  305. const [login, setLogin] = useState("");
  306. const [password, setPassword] = useState("");
  307. return (
  308. <section className="login">
  309. <h2>Логин</h2>
  310. <input type="text"
  311. placeholder="Введите ваш логин"
  312. onChange={evt => setLogin(evt.target.value)} /><br />
  313. <input type="password"
  314. placeholder="Введите ваш пароль"
  315. onChange={evt => setPassword(evt.target.value)} /><br />
  316. <button onClick={() => fullLogin(login, password)}>Залогинится</button>
  317. </section>
  318. );
  319. };
  320. const CLogin = connect(null, {fullLogin: actionFullLogin})(Login);
  321. let Registration = function({fullRegister}) {
  322. const [login, setLogin] = useState("");
  323. const [nick, setNick] = useState("");
  324. const [password, setPassword] = useState("");
  325. return (
  326. <section className="registration">
  327. <h2>Регистрация</h2>
  328. <input type="text"
  329. placeholder="Введите ваш логин"
  330. onChange={evt => setLogin(evt.target.value)} /><br/>
  331. <input type="text"
  332. placeholder="Введите ваш никнейм"
  333. onChange={evt => setNick(evt.target.value)} /><br/>
  334. <input type="password"
  335. placeholder="Введите ваш пароль"
  336. onChange={evt => setPassword(evt.target.value)} /><br/>
  337. <button onClick={() => fullRegister(login, password, nick)}>Зарегистрироваться</button>
  338. </section>
  339. );
  340. };
  341. const CRegistration = connect(null, {fullRegister: actionFullRegister})(Registration)
  342. let CardGood = function({good: {count, good}, cartChange, cartDelete}) {
  343. const [value, setValue] = useState(count);
  344. return (
  345. <tr>
  346. <td>
  347. <img className="cart__img"
  348. src={`${backendURL}/${good?.images[0]?.url}`} />
  349. </td>
  350. <td>{good?.name}</td>
  351. <td>Цена: {good?.price}</td>
  352. <td>Количество:
  353. <input type="number"
  354. value={value}
  355. onChange={evt => {
  356. setValue(evt.target.value);
  357. cartChange(good, evt.target.value);
  358. }}/>
  359. </td>
  360. <td>
  361. <button onClick={() => cartDelete(good)}>Удоли</button>
  362. </td>
  363. </tr>
  364. );
  365. };
  366. const CCardGood = connect(null, {cartChange: actionCartChange,
  367. cartDelete: actionCartDelete})(CardGood);
  368. let Cart = function({cart, makeOrder}) {
  369. return (
  370. <section className="cart">
  371. <h2>Корзина</h2>
  372. <table>
  373. {cart.map(good => <CCardGood good={good} />)}
  374. </table>
  375. <button onClick={() => makeOrder()}>Сделать заказ</button>
  376. </section>
  377. );
  378. };
  379. const CCart = connect(state => ({cart: Object.values(state.cart) || []}),
  380. {makeOrder: actionOrders})(Cart);
  381. let OrderGood = function({good: {count, good}}) {
  382. return (
  383. <li>
  384. <ul className="dashboard__good-list">
  385. <li>
  386. <img className="dashboard__img"
  387. src={`${backendURL}/${good?.images[0]?.url}`} />
  388. </li>
  389. <li>{good?.name}</li>
  390. <li>Цена: {good?.price}</li>
  391. <li>Количество: {count}</li>
  392. </ul>
  393. </li>
  394. );
  395. };
  396. let Order = function({order: {createdAt, orderGoods}}) {
  397. return (
  398. <tr>
  399. <td className="dashboard__date">{(new Date(+createdAt)).toLocaleString()}</td>
  400. <td>
  401. <ul className="dashboard__list">
  402. {orderGoods.map(orderGood => <OrderGood good={orderGood} />)}
  403. </ul>
  404. </td>
  405. </tr>
  406. );
  407. }
  408. let Dashboard = function({orders}) {
  409. return (
  410. <table>
  411. {orders.map(order => <Order order={order} />)}
  412. </table>
  413. );
  414. };
  415. const CDashboardTable = connect(state => ({orders: state.promise?.ordersFind?.payload || []}))(Dashboard)
  416. let PreDashboard = function({orderFind}) {
  417. orderFind();
  418. return (
  419. <section className="dashboard">
  420. <h2>История заказов</h2>
  421. <CDashboardTable />
  422. </section>
  423. );
  424. };
  425. const CDashboard = connect(null, {orderFind: actionOrdersFind})(PreDashboard);
  426. let Content = function() {
  427. return (
  428. <Switch>
  429. <Route path="/" exact component={MainPage} />
  430. <Route path="/category/:_id" exact component={CCategories} />
  431. <Route path="/good/:_id" exact component={CGoodPage} />
  432. <Route path="/login" exact component={CLogin} />
  433. <Route path="/registration" exact component={CRegistration} />
  434. <Route path="/cart" exact component={CCart} />
  435. <Route path="/dashboard" exact component={CDashboard} />
  436. </Switch>
  437. );
  438. };
  439. let Main = function() {
  440. return (
  441. <main className="main">
  442. <CRootCategories />
  443. <Content />
  444. </main>
  445. );
  446. };
  447. const history = createHistory()
  448. function App() {
  449. return (
  450. <Router history={history}>
  451. <Provider store={store}>
  452. <div className="App">
  453. <Header />
  454. <Main />
  455. </div>
  456. </Provider>
  457. </Router>
  458. );
  459. }
  460. export default App;