App.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. import logo from "./logo.svg";
  2. import React, { useState } from "react";
  3. import "./App.css";
  4. import thunk from "redux-thunk";
  5. import { createStore, combineReducers, applyMiddleware } from "redux";
  6. import { Provider, connect } from "react-redux";
  7. const store = createStore(
  8. combineReducers({
  9. auth: authReducer,
  10. promise: promiseReducer,
  11. cart: localStoreReducer(cartReducer, "cart"),
  12. }),
  13. applyMiddleware(thunk)
  14. );
  15. function jwtDecode(token) {
  16. try {
  17. return JSON.parse(atob(token.split(".")[1]));
  18. } catch (e) {}
  19. }
  20. const getGQL = (url) => (query, variables) =>
  21. fetch(url, {
  22. method: "POST",
  23. headers: {
  24. "Content-Type": "application/json",
  25. // 'Accept' : 'application/json',
  26. ...(localStorage.authToken
  27. ? { Authorization: "Bearer " + localStorage.authToken }
  28. : {}),
  29. },
  30. body: JSON.stringify({ query, variables }),
  31. })
  32. .then((res) => res.json())
  33. .then((data) => {
  34. if (data.data) {
  35. return Object.values(data.data)[0];
  36. } else throw new Error(JSON.stringify(data.errors));
  37. });
  38. const backendURL = "http://shop-roles.node.ed.asmer.org.ua";
  39. const gql = getGQL(backendURL + "/graphql");
  40. function authReducer(state, { type, token }) {
  41. if (state === undefined) {
  42. if (localStorage.authToken) {
  43. type = "AUTH_LOGIN";
  44. token = localStorage.authToken;
  45. }
  46. }
  47. if (type === "AUTH_LOGIN") {
  48. let payload = jwtDecode(token);
  49. if (payload) {
  50. localStorage.authToken = token;
  51. return { token, payload };
  52. }
  53. }
  54. if (type === "AUTH_LOGOUT") {
  55. localStorage.removeItem("authToken");
  56. return {};
  57. }
  58. return state || {};
  59. }
  60. const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
  61. const actionAuthLogout = () => (dispatch) => {
  62. dispatch({ type: "AUTH_LOGOUT" });
  63. localStorage.removeItem("authToken");
  64. };
  65. function promiseReducer(state = {}, { type, name, status, payload, error }) {
  66. if (type === "PROMISE") {
  67. return {
  68. ...state,
  69. [name]: { status, payload, error },
  70. };
  71. }
  72. return state;
  73. }
  74. const actionPending = (name) => ({
  75. type: "PROMISE",
  76. status: "PENDING",
  77. name,
  78. });
  79. const actionFulfilled = (name, payload) => ({
  80. type: "PROMISE",
  81. status: "FULFILLED",
  82. name,
  83. payload,
  84. });
  85. const actionRejected = (name, error) => ({
  86. type: "PROMISE",
  87. status: "REJECTED",
  88. name,
  89. error,
  90. });
  91. const actionPromise = (name, promise) => async (dispatch) => {
  92. try {
  93. dispatch(actionPending(name));
  94. let payload = await promise;
  95. dispatch(actionFulfilled(name, payload));
  96. return payload;
  97. } catch (e) {
  98. dispatch(actionRejected(name, e));
  99. }
  100. };
  101. function cartReducer(state = {}, { type, count = 1, good }) {
  102. if (type === "CART_ADD") {
  103. return {
  104. ...state,
  105. [good._id]: { count: count + (state[good._id]?.count || 0), good },
  106. };
  107. }
  108. if (type === "CART_DELETE") {
  109. if (state[good._id].count > 1) {
  110. return {
  111. ...state,
  112. [good._id]: {
  113. count: -count + (state[good._id]?.count || 0),
  114. good,
  115. },
  116. };
  117. }
  118. if (state[good._id].count === 1) {
  119. let { [good._id]: id1, ...newState } = state; //o4en strashnoe koldunstvo
  120. //delete newState[good._id]
  121. return newState;
  122. }
  123. }
  124. if (type === "CART_CLEAR") {
  125. return {};
  126. }
  127. if (type === "CART_REMOVE") {
  128. // let newState = {...state}
  129. let { [good._id]: id1, ...newState } = state; //o4en strashnoe koldunstvo
  130. //delete newState[good._id]
  131. return newState;
  132. }
  133. return state;
  134. }
  135. const actionCartAdd = (good, count = 1) => ({ type: "CART_ADD", good, count });
  136. const actionCartDelete = (good) => ({ type: "CART_DELETE", good });
  137. const actionCartClear = () => ({ type: "CART_CLEAR" });
  138. const actionCartRemove = (good) => ({ type: "CART_REMOVE", good });
  139. function localStoreReducer(reducer, localStorageKey) {
  140. function localStoredReducer(state, action) {
  141. // Если state === undefined, то достать старый state из local storage
  142. if (state === undefined) {
  143. try {
  144. return JSON.parse(localStorage[localStorageKey]);
  145. } catch (e) {}
  146. }
  147. const newState = reducer(state, action);
  148. // Сохранить newState в local storage
  149. localStorage[localStorageKey] = JSON.stringify(newState);
  150. return newState;
  151. }
  152. return localStoredReducer;
  153. }
  154. const actionRootCats = () =>
  155. actionPromise(
  156. "rootCats",
  157. gql(
  158. `query {
  159. CategoryFind(query: "[{\\"parent\\":null}]"){
  160. _id name
  161. }
  162. }`
  163. )
  164. );
  165. const actionCatById = (_id) =>
  166. actionPromise(
  167. "catById",
  168. gql(
  169. `query catById($q: String){
  170. CategoryFindOne(query: $q){
  171. _id name subCategories {
  172. name _id
  173. }
  174. goods {
  175. _id name price images {
  176. url
  177. }
  178. }
  179. }
  180. }`,
  181. { q: JSON.stringify([{ _id }]) }
  182. )
  183. );
  184. const actionLogin = (login, password) =>
  185. actionPromise(
  186. "actionLogin",
  187. gql(
  188. `query log($login:String, $password:String){
  189. login(login:$login, password:$password)
  190. }`,
  191. { login, password }
  192. )
  193. );
  194. const actionGoodById = (_id) =>
  195. actionPromise(
  196. "GoodFineOne",
  197. gql(
  198. `query goodByid($goodId: String) {
  199. GoodFindOne(query: $goodId) {
  200. _id
  201. name
  202. price
  203. description
  204. images {
  205. url
  206. }
  207. }
  208. }`,
  209. { goodId: JSON.stringify([{ _id }]) }
  210. )
  211. );
  212. store.dispatch(actionRootCats());
  213. store.dispatch(actionCatById("6262ca7dbf8b206433f5b3d1"));
  214. const actionFullLogin = (log, pass) => async (dispatch) => {
  215. let token = await dispatch(
  216. actionPromise(
  217. "login",
  218. gql(
  219. `query login($login: String, $password: String) {
  220. login(login: $login, password: $password)
  221. }`,
  222. { login: log, password: pass }
  223. )
  224. )
  225. );
  226. if (token) {
  227. dispatch(actionAuthLogin(token));
  228. }
  229. };
  230. const actionFullRegister = (login, password) => async (dispatch) => {
  231. let user = await dispatch(
  232. actionPromise(
  233. "register",
  234. gql(
  235. `mutation register($login: String, $password: String) {
  236. UserUpsert(user: {login: $login, password: $password}) {
  237. _id
  238. login
  239. }
  240. }`,
  241. { login: login, password: password }
  242. )
  243. )
  244. );
  245. if (user) {
  246. dispatch(actionFullLogin(login, password));
  247. }
  248. };
  249. const actionOrder = () => async (dispatch, getState) => {
  250. let { cart } = getState();
  251. const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({
  252. good: { _id },
  253. count,
  254. }));
  255. let result = await dispatch(
  256. actionPromise(
  257. "order",
  258. gql(
  259. `
  260. mutation newOrder($order:OrderInput){
  261. OrderUpsert(order:$order)
  262. { _id total }
  263. }
  264. `,
  265. { order: { orderGoods } }
  266. )
  267. )
  268. );
  269. if (result?._id) {
  270. dispatch(actionCartClear());
  271. document.location.hash = "#/cart/";
  272. alert("Purchase completed");
  273. }
  274. };
  275. const orderHistory = () =>
  276. actionPromise(
  277. "history",
  278. gql(` query OrderFind{
  279. OrderFind(query:"[{}]"){
  280. _id total createdAt orderGoods{
  281. count good{
  282. _id name price images{
  283. url
  284. }
  285. }
  286. owner{
  287. _id login
  288. }
  289. }
  290. }
  291. }
  292. `)
  293. );
  294. const LoginForm = ({ onLogin }) => {
  295. const [login, setLogin] = useState("");
  296. const [password, setPassword] = useState("");
  297. const checkDisable = () => {
  298. return !(login !== "" && password.match(/\w{8,30}$/));
  299. };
  300. const Authorization = () => {
  301. onLogin(login, password);
  302. };
  303. return (
  304. <div className="formBlock">
  305. <p>Enter login:</p>
  306. <input
  307. type="text"
  308. value={login}
  309. onChange={(e) => setLogin(e.target.value)}
  310. />
  311. <p>Enter password:</p>
  312. <input
  313. type="password"
  314. value={password}
  315. onChange={(e) => setPassword(e.target.value)}
  316. />
  317. <button disabled={checkDisable()} onClick={Authorization}>
  318. Log In
  319. </button>
  320. </div>
  321. );
  322. };
  323. if (localStorage.authToken) {
  324. store.dispatch(actionAuthLogin(localStorage.authToken));
  325. }
  326. store.subscribe(() => console.log(store.getState()));
  327. const cat = [
  328. {
  329. _id: "62d57ab8b74e1f5f2ec1a148",
  330. name: "Motorola Razr 5G 8/256GB Graphite",
  331. price: 3500,
  332. },
  333. {
  334. _id: "62d57c4db74e1f5f2ec1a14a",
  335. name: "Смартфон Google Pixel 6 Pro 12/128GB Stormy Black",
  336. price: 2800,
  337. },
  338. {
  339. _id: "62d58318b74e1f5f2ec1a14e",
  340. name: "Microsoft Surface Duo 2 8GB/256GB",
  341. price: 4500,
  342. },
  343. {
  344. _id: "62d5869bb74e1f5f2ec1a150",
  345. name: "Смартфон Poco F3 6/128GB EU Arctic White",
  346. price: 1800,
  347. },
  348. {
  349. _id: "62d58810b74e1f5f2ec1a152",
  350. name: "Мобильный телефон Xiaomi Redmi Note 9 4G (Redmi 9t EU)",
  351. price: 800,
  352. },
  353. {
  354. _id: "62d5a7deb74e1f5f2ec1a154",
  355. name: "LG V50 black REF",
  356. price: 900,
  357. },
  358. ];
  359. const rootCats = [
  360. {
  361. name: "test3",
  362. },
  363. {
  364. name: "Tools",
  365. },
  366. {
  367. name: "Tomatoes",
  368. },
  369. {
  370. name: "123",
  371. },
  372. {
  373. name: "iPhone",
  374. },
  375. {
  376. name: "Samsung",
  377. },
  378. {
  379. name: "Smartphone",
  380. },
  381. {
  382. name: "Large home appliances",
  383. },
  384. {
  385. name: "Garden",
  386. },
  387. {
  388. name: "Children's products",
  389. },
  390. {
  391. name: " Hobbies and sports",
  392. },
  393. {
  394. name: "Sale",
  395. },
  396. ];
  397. const CategoryMenu = ({rootCats = []}) => {
  398. return (
  399. <>
  400. <aside id="aside">
  401. <ul>
  402. {rootCats.map((cat) => (
  403. <CategoryMenuItem name={cat.name} />
  404. ))}
  405. </ul>
  406. </aside>
  407. </>
  408. );
  409. };
  410. const CCategoryMenu = connect((state) => ({rootCats: state.promise?.rootCats.payload}))(CategoryMenu)
  411. const CategoryMenuItem = ({ name }) => {
  412. return (
  413. <>
  414. <h2>{name}</h2>
  415. </>
  416. );
  417. };
  418. const Category = ({cat = []}) => {
  419. return (
  420. <div className="cardBlock">
  421. {cat.map((item) => (
  422. <GoodCard name={item.name} price={item.price} />
  423. ))}
  424. </div>
  425. );
  426. };
  427. const CCategory = connect((state) => ({cat: state.promise?.catById.payload?.goods}))(Category)
  428. const GoodCard = ({ name, price }) => {
  429. return (
  430. <div className="card">
  431. <h3>{name}</h3>
  432. <h4>{price}</h4>
  433. </div>
  434. );
  435. };
  436. const CLoginForm = connect(null, { onLogin: actionFullLogin })(LoginForm);
  437. const Spoiler = ({header = '+', open, children}) => {
  438. const [isOpen, setIsOpen] = useState(open);
  439. return (
  440. <div>
  441. <div onClick={() => {
  442. setIsOpen(!isOpen);
  443. }}>
  444. <span>{header}</span>
  445. {isOpen && (
  446. <div>
  447. {children}
  448. </div>
  449. )}
  450. </div>
  451. </div>
  452. );
  453. };
  454. const SpoilerParent = () => {
  455. return (
  456. <div>
  457. <Spoiler header={<h1>Заголовок</h1>} open>
  458. Контент 1
  459. <p>
  460. лорем ипсум траливали и тп.лорем ипсум траливали и тп.лорем ипсум траливали и тп.лорем ипсум траливали и тп.лорем ипсум траливали и тп.
  461. </p>
  462. </Spoiler>
  463. <Spoiler>
  464. <h2>Контент 2</h2>
  465. <p>
  466. лорем ипсум траливали и тп.лорем ипсум траливали и тп.лорем ипсум траливали и тп.лорем ипсум траливали и тп.лорем ипсум траливали и тп.
  467. </p>
  468. </Spoiler>
  469. </div>
  470. );
  471. };
  472. class RangeInput extends React.Component {
  473. constructor(props) {
  474. super(props);
  475. this.state = {
  476. value: '',
  477. };
  478. }
  479. inputChange = (e) => {
  480. this.setState({value: e.target.value});
  481. };
  482. render() {
  483. const {min, max} = this.props;
  484. const hasError = this.state.value.length > max || this.state.value.length < min;
  485. return (
  486. <div>
  487. <input onChange={this.inputChange} className={hasError ? 'error' : ''}/>
  488. </div>
  489. );
  490. }
  491. }
  492. const RangeInputParent = () => {
  493. return(
  494. <RangeInput min={2} max={10} />
  495. )
  496. }
  497. class PasswordConfirm extends React.Component {
  498. constructor(props) {
  499. super(props);
  500. this.state = {
  501. password: '',
  502. passwordConfirm: '',
  503. };
  504. }
  505. inputChange = (e) => {
  506. this.setState({[e.target.name]: e.target.value})
  507. };
  508. render() {
  509. const {min} = this.props;
  510. const arePasswordsEqual = this.state.password === this.state.passwordConfirm;
  511. const hasErrorPsw = this.state.password.length < min || !arePasswordsEqual;
  512. const hasErrorPswConfirm = this.state.passwordConfirm.length < min || !arePasswordsEqual;
  513. return (
  514. <div className="PasswordConfirmWrapper">
  515. <input placeholder={'password'}
  516. name={'password'}
  517. type={'password'}
  518. className={hasErrorPsw ? 'error' : ''}
  519. onChange={this.inputChange}
  520. />
  521. <input placeholder={'confirm password'}
  522. name={'passwordConfirm'}
  523. type={'password'}
  524. className={hasErrorPswConfirm ? 'error' : ''}
  525. onChange={this.inputChange}/>
  526. </div>
  527. );
  528. }
  529. }
  530. class PasswordConfirmParent extends React.Component{
  531. render() {
  532. return(
  533. <div>
  534. <PasswordConfirm min={2} />
  535. </div>
  536. )
  537. }
  538. }
  539. function App() {
  540. return (
  541. <Provider store={store}>
  542. <div className="App">
  543. <SpoilerParent></SpoilerParent>
  544. <RangeInputParent></RangeInputParent>
  545. <PasswordConfirmParent></PasswordConfirmParent>
  546. <CLoginForm />
  547. <CCategoryMenu />
  548. <CCategory />
  549. </div>
  550. </Provider>
  551. );
  552. }
  553. export default App;