Browse Source

Changes and additions to JSX and online store on React

Bonyant 2 years ago
parent
commit
08e33147a2

+ 121 - 24
1/package-lock.json

@@ -1855,15 +1855,10 @@
         "@babel/runtime": "^7.6.2"
       }
     },
-    "@restart/context": {
-      "version": "2.1.4",
-      "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
-      "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q=="
-    },
     "@restart/hooks": {
-      "version": "0.3.27",
-      "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz",
-      "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==",
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.5.tgz",
+      "integrity": "sha512-tLGtY0aHeIfT7aPwUkvQuhIy3+q3w4iqmUzFLPlOAf/vNUacLaBt1j/S//jv/dQhenRh8jvswyMojCwmLvJw8A==",
       "requires": {
         "dequal": "^2.0.2"
       }
@@ -1883,16 +1878,6 @@
         "prop-types": "^15.7.2",
         "uncontrollable": "^7.2.1",
         "warning": "^4.0.3"
-      },
-      "dependencies": {
-        "@restart/hooks": {
-          "version": "0.4.5",
-          "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.5.tgz",
-          "integrity": "sha512-tLGtY0aHeIfT7aPwUkvQuhIy3+q3w4iqmUzFLPlOAf/vNUacLaBt1j/S//jv/dQhenRh8jvswyMojCwmLvJw8A==",
-          "requires": {
-            "dequal": "^2.0.2"
-          }
-        }
       }
     },
     "@rollup/plugin-babel": {
@@ -6076,6 +6061,19 @@
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
       "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
     },
+    "history": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+      "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+      "requires": {
+        "@babel/runtime": "^7.1.2",
+        "loose-envify": "^1.2.0",
+        "resolve-pathname": "^3.0.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0",
+        "value-equal": "^1.0.1"
+      }
+    },
     "hoist-non-react-statics": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -8402,6 +8400,15 @@
       "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
       "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
     },
+    "mini-create-react-context": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
+      "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
+      "requires": {
+        "@babel/runtime": "^7.12.1",
+        "tiny-warning": "^1.0.3"
+      }
+    },
     "mini-css-extract-plugin": {
       "version": "2.4.5",
       "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz",
@@ -10077,14 +10084,13 @@
       }
     },
     "react-bootstrap": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.0.3.tgz",
-      "integrity": "sha512-gIRzQf170SGKS09azr8Cl9+8FxBf3J9jyFf/8sWNWXtRcgbRzTog9bFCS4ua5Wv1U/A7W7hRlnrKxzy+wl+5Rw==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.0.4.tgz",
+      "integrity": "sha512-sfxhLKY/P5oeqkcxI4Q3SkJKLBq/7pv1wGykGlmCLWp4Pll3HMVl5VRtVbKsE4FzGsZGhXXauhi2HhRbmWLwBA==",
       "requires": {
         "@babel/runtime": "^7.14.0",
-        "@restart/context": "^2.1.4",
-        "@restart/hooks": "^0.3.26",
-        "@restart/ui": "^0.2.3",
+        "@restart/hooks": "^0.4.5",
+        "@restart/ui": "^0.2.5",
         "@types/invariant": "^2.2.33",
         "@types/prop-types": "^15.7.3",
         "@types/react": ">=16.14.8",
@@ -10211,6 +10217,31 @@
       "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
       "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
     },
+    "react-overlays": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz",
+      "integrity": "sha512-eCN2s2/+GVZzpnId4XVWtvDPYYBD2EtOGP74hE+8yDskPzFy9+pV1H3ZZihxuRdEbQzzacySaaDkR7xE0ydl4Q==",
+      "requires": {
+        "@babel/runtime": "^7.13.8",
+        "@popperjs/core": "^2.8.6",
+        "@restart/hooks": "^0.3.26",
+        "@types/warning": "^3.0.0",
+        "dom-helpers": "^5.2.0",
+        "prop-types": "^15.7.2",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      },
+      "dependencies": {
+        "@restart/hooks": {
+          "version": "0.3.27",
+          "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz",
+          "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==",
+          "requires": {
+            "dequal": "^2.0.2"
+          }
+        }
+      }
+    },
     "react-redux": {
       "version": "7.2.6",
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
@@ -10236,6 +10267,52 @@
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
       "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
     },
+    "react-router": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
+      "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
+      "requires": {
+        "@babel/runtime": "^7.12.13",
+        "history": "^4.9.0",
+        "hoist-non-react-statics": "^3.1.0",
+        "loose-envify": "^1.3.1",
+        "mini-create-react-context": "^0.4.0",
+        "path-to-regexp": "^1.7.0",
+        "prop-types": "^15.6.2",
+        "react-is": "^16.6.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "path-to-regexp": {
+          "version": "1.8.0",
+          "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+          "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+          "requires": {
+            "isarray": "0.0.1"
+          }
+        }
+      }
+    },
+    "react-router-dom": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
+      "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
+      "requires": {
+        "@babel/runtime": "^7.12.13",
+        "history": "^4.9.0",
+        "loose-envify": "^1.3.1",
+        "prop-types": "^15.6.2",
+        "react-router": "5.2.1",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      }
+    },
     "react-scripts": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz",
@@ -10710,6 +10787,11 @@
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
     },
+    "resolve-pathname": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
+      "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
+    },
     "resolve-url-loader": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz",
@@ -11863,6 +11945,16 @@
       "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
       "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
     },
+    "tiny-invariant": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
+      "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
+    },
+    "tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+    },
     "tmp": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
@@ -12184,6 +12276,11 @@
         "spdx-expression-parse": "^3.0.0"
       }
     },
+    "value-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
+      "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
+    },
     "vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

+ 3 - 1
1/package.json

@@ -8,9 +8,11 @@
     "@testing-library/user-event": "^13.5.0",
     "bootstrap": "^5.1.3",
     "react": "^17.0.2",
-    "react-bootstrap": "^2.0.3",
+    "react-bootstrap": "^2.0.4",
     "react-dom": "^17.0.2",
+    "react-overlays": "^5.1.1",
     "react-redux": "^7.2.6",
+    "react-router-dom": "^5.3.0",
     "react-scripts": "5.0.0",
     "redux": "^4.1.2",
     "redux-thunk": "^2.4.1",

+ 258 - 269
1/src/App.js

@@ -1,5 +1,5 @@
 /* eslint-disable jsx-a11y/alt-text */
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
 import logoDefault from "./logo.svg";
 import "./App.scss";
 import { Provider, connect } from "react-redux";
@@ -7,6 +7,9 @@ import { createStore, combineReducers, applyMiddleware } from "redux";
 import thunk from "redux-thunk";
 import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
 import { Button } from "react-bootstrap";
+import { Router, Route, Link, Switch, withRouter } from "react-router-dom";
+import { createBrowserHistory } from "history";
+import { Navbar, NavDropdown, Container, Nav } from "react-bootstrap";
 
 const jwtDecode = (token) => {
   try {
@@ -55,15 +58,35 @@ const actionCatById = (_id) =>
     "catById",
     gql(
       `query catById($q: String){
-          CategoryFindOne(query: $q){
-              subCategories{name, _id}
-              _id name goods {
-                  _id name price images {
-                      url
-                  }
-              }
+      CategoryFindOne(query: $q){
+          _id name,
+          goods{
+            _id name price images {
+               url
+            }
+          },
+          subCategories{
+            name, subCategories{
+              name
+            }
           }
-      }`,
+        }
+  }`,
+      { q: JSON.stringify([{ _id }]) }
+    )
+  );
+
+const actionGoodById = (_id) =>
+  actionPromise(
+    "goodById",
+    gql(
+      `query goodById($q: String){
+  GoodFindOne(query: $q){
+          _id name description price images{
+            url
+          }
+      }
+  }`,
       { q: JSON.stringify([{ _id }]) }
     )
   );
@@ -104,15 +127,34 @@ const actionLogin = (login, password) =>
     )
   );
 
+const actionRegister = (login, password) =>
+  actionPromise(
+    "registration",
+    gql(
+      `mutation reg2($user:UserInput) {
+      UserUpsert(user:$user) {
+      _id login
+      }
+  }
+  `,
+      { user: { login: login, password: password } }
+    )
+  );
+
 const actionFullLogin = (login, password) => async (dispatch) => {
-  // console.log(login, password);
   let token = await dispatch(actionLogin(login, password));
-  // console.log(token);
   if (token) {
     dispatch(actionAuthLogin(token));
   }
 };
 
+const actionFullRegister = (login, password) => async (dispatch) => {
+  let check = await dispatch(actionRegister(login, password));
+  if (check) {
+    dispatch(actionFullLogin(login, password));
+  }
+};
+
 const actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
 const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
 
@@ -219,196 +261,85 @@ const store = createStore(
 
 store.subscribe(() => console.log(store.getState()));
 store.dispatch(actionRootCats());
-store.dispatch(actionCatById("5dc458985df9d670df48cc47"));
 
 const Logo = ({ logo = logoDefault }) => (
-  <a href="https://www.google.com.ua/?hl=ru" className="Logo">
-    <img src={logo} />
-  </a>
+  <Link to="/" className="navbar-brand">
+    <img src={logo} width="30" height="30" alt="" />
+    React Site
+  </Link>
 );
 
 const Cart = ({ cart }) => {
   let count = 0;
   let sum = Object.entries(cart).map(([, val]) => val.count);
   count = sum.reduce((a, b) => a + b, 0);
-  return <div className="Cart">Товаров в корзине: {count}</div>;
+  return (
+    <Link className="nav-link" to="/cart">
+      Корзина: {count} товара
+    </Link>
+  );
 };
 
 const CCart = connect(({ cart }) => ({ cart }))(Cart);
 
-const Header = ({ logo = logoDefault }) => (
-  <header>
-    <Logo logo={logo} />
-    <CCart />
-  </header>
-);
-
-const Footer = ({ logo = logoDefault }) => (
-  <footer>
-    <Logo logo={logo} />
-  </footer>
-);
-
-const defaultRootCats = [
-  { _id: "5dc49f4d5df9d670df48cc64", name: "Airconditions" },
-  { _id: "5dc458985df9d670df48cc47", name: "     Smartphones" },
-  { _id: "5dc4b2553f23b553bf354101", name: "Крупная бытовая техника" },
-  { _id: "5dcac1b56d09c45440d14cf8", name: "Макароны" },
-];
-
 const RootCategory = ({ cat: { _id, name } = {} }) => (
-  <li>
-    <a href={`#/${_id}`}>{name}</a>
-  </li>
+  <NavDropdown.Item
+    componentclass={Link}
+    href={`/category/${_id}`}
+    to={`/category/${_id}`}
+  >
+    {name}
+  </NavDropdown.Item>
 );
-const RootCategories = ({ cats = defaultRootCats }) => (
-  <ul className="RootCategories">
-    {cats.map((cat) => (
-      <RootCategory cat={cat} />
+
+const RootCategories = ({ cats }) => (
+  <NavDropdown title="Категории" id="collasible-nav-dropdown">
+    {cats.map((cat, i) => (
+      <RootCategory cat={cat} key={i} />
     ))}
-  </ul>
+  </NavDropdown>
 );
 
 const CRootCategories = connect((state) => ({
   cats: state.promise.rootCats?.payload || [],
 }))(RootCategories);
 
-const Aside = () => (
-  <aside>
-    <CRootCategories />
-  </aside>
+const Header = () => (
+  <Navbar collapseOnSelect expand="lg" bg="dark" variant="dark">
+    <Container>
+      <Navbar.Brand>
+        <Logo />
+      </Navbar.Brand>
+      <Navbar.Toggle aria-controls="responsive-navbar-nav" />
+      <Navbar.Collapse id="responsive-navbar-nav">
+        <Nav className="me-auto">
+          <CRootCategories />
+        </Nav>
+        <Nav>
+          <CCart />
+          <СAuthorizations />
+        </Nav>
+      </Navbar.Collapse>
+    </Container>
+  </Navbar>
+);
+
+const Footer = ({ logo = logoDefault }) => (
+  <footer>
+    <Logo logo={logo} />
+  </footer>
 );
 
 const Content = ({ children }) => <div className="Content">{children}</div>;
-const defaultCat = {
-  subCategories: null,
-  _id: "5dc458985df9d670df48cc47",
-  name: "     Smartphones",
-  goods: [
-    {
-      _id: "5dc45c3d5df9d670df48cc4a",
-      name: "AABBA",
-      price: 1000,
-      images: [
-        {
-          url: "images/1b2b030516d2c7c5b43d5b5b5ec0e25a",
-        },
-      ],
-    },
-    {
-      _id: "5dc45d0b5df9d670df48cc4b",
-      name: "Apple iPhone X 64GB Space Gray",
-      price: 5000,
-      images: [
-        {
-          url: "images/00505f5f08ac113874318dee67975aa9",
-        },
-      ],
-    },
-    {
-      _id: "5dc4617c5df9d670df48cc4c",
-      name: "Apple iPhone XR 64Gb Black",
-      price: 750,
-      images: [
-        {
-          url: "images/0671607428c0cb401a899a469395fe2f",
-        },
-      ],
-    },
-    {
-      _id: "5dc49faf5df9d670df48cc66",
-      name: "Xiaomi Redmi Note 7 4/64GB Neptune Bluel",
-      price: 3150,
-      images: [
-        {
-          url: "images/a915392b85ccad7f8572f7a597a9104d",
-        },
-      ],
-    },
-    {
-      _id: "5dc4a06e5df9d670df48cc67",
-      name: "Xiaomi Mi A3 4/128GB Not just Blue",
-      price: 220,
-      images: [
-        {
-          url: "images/32ea31cc1251b10314bfce25c4c81419",
-        },
-      ],
-    },
-    {
-      _id: "5dc4a1535df9d670df48cc68",
-      name: "Samsung Galaxy S10e 128GB Yellow",
-      price: 700,
-      images: [
-        {
-          url: "images/dee2c46e1ffafe70083831e5e631b43a",
-        },
-      ],
-    },
-    {
-      _id: "5dc4a1e35df9d670df48cc69",
-      name: "Samsung Galaxy A80 2019 8/128GB Black",
-      price: 600,
-      images: [
-        {
-          url: "images/d62dc5e3446f427f59a8b5fe922e4ac6",
-        },
-      ],
-    },
-    {
-      _id: "5dc4a26c5df9d670df48cc6a",
-      name: "Samsung Galaxy S10e 128GB Black",
-      price: 700,
-      images: [
-        {
-          url: "images/298583a975a33311c3f55cd325586a77",
-        },
-      ],
-    },
-    {
-      _id: "5dc4a3e15df9d670df48cc6b",
-      name: "Apple iPhone 11 Pro Max 64GB Gold",
-      price: 1500,
-      images: [
-        {
-          url: "images/b599634ebfecf2a19d900e22434bedbd",
-        },
-      ],
-    },
-    {
-      _id: "5dc4a4365df9d670df48cc6c",
-      name: "Apple iPhone XS Max 256GB Gold",
-      price: 1300,
-      images: [
-        {
-          url: "images/63c4a052377862494e33746b375903f6",
-        },
-      ],
-    },
-    {
-      _id: "61b1056cc750c12ba6ba4522",
-      name: "iPhone ",
-      price: 1000,
-      images: [
-        {
-          url: "images/cc23c15a3ae1ac60582785ebf9b3d207",
-        },
-      ],
-    },
-    {
-      _id: "61b105f9c750c12ba6ba4524",
-      name: "iPhone ",
-      price: 1200,
-      images: [
-        {
-          url: "images/50842a3af34bfa28be037aa644910d07",
-        },
-      ],
-    },
-  ],
-};
 
-const SubCategories = ({ cats }) => <></>;
+const SubCategories = ({ cats } = {}) => {
+  console.log(...cats);
+  return (
+    <div className="SubCategories">
+      <h3>Подкатегории</h3>
+    </div>
+  );
+};
 
 const GoodCard = ({ good: { _id, name, price, images } = {}, onCartAdd }) => (
   <div className="GoodCard">
@@ -416,9 +347,11 @@ const GoodCard = ({ good: { _id, name, price, images } = {}, onCartAdd }) => (
     {images && images[0] && images[0].url && (
       <img src={backURL + "/" + images[0].url} />
     )}
-    <strong>{price}</strong>
+    <p>Цена: {price}</p>
+    <Link to={`/good/${_id}`}>Посмотреть</Link>
+    <br />
     <Button
-      variant="primary"
+      variant="success"
       onClick={() => onCartAdd({ _id, name, price, images })}
     >
       +
@@ -428,14 +361,12 @@ const GoodCard = ({ good: { _id, name, price, images } = {}, onCartAdd }) => (
 
 const CGoodCard = connect(null, { onCartAdd: actionCartAdd })(GoodCard);
 
-const Category = ({
-  cat: { _id, name, goods, subCategories } = defaultCat,
-}) => (
+const Category = ({ cat: { _id, name, goods, subCategories } }) => (
   <div className="Category">
     <h1>{name}</h1>
     {subCategories && <SubCategories cats={subCategories} />}
-    {(goods || []).map((good) => (
-      <CGoodCard good={good} />
+    {(goods || []).map((good, i) => (
+      <CGoodCard good={good} key={i} />
     ))}
   </div>
 );
@@ -468,7 +399,7 @@ const CartGood = ({
       >
         Delete
       </Button>
-      <label for="CountField">Change count</label>
+      <label hrmlFor="CountField">Change count</label>
       <input
         type="number"
         value={count}
@@ -487,7 +418,7 @@ const CCartGood = connect(null, {
 
 const CartPage = ({ cart, clearCart }) => (
   <div className="CartMain">
-    <h1>Корзина</h1>
+    <h1>Cart</h1>
 
     {Object.entries(cart).map(([, cartGood]) => (
       <CCartGood cartGood={cartGood} />
@@ -507,7 +438,7 @@ const CCartPage = connect((state) => ({ cart: state.cart }), {
   clearCart: actionCartClear,
 })(CartPage);
 
-const LoginForm = ({ onLogin }) => {
+const LoginForm = ({ onLogin, onRegister }) => {
   const [login, setLogin] = useState("");
   const [password, setPassword] = useState("");
   return (
@@ -525,109 +456,167 @@ const LoginForm = ({ onLogin }) => {
         placeholder="Password"
       />
       {
-        <button
+        <Button
+          variant="success"
           disabled={login.length < 1 || password.length < 1 ? true : false}
           onClick={() => onLogin(login, password)}
         >
           Login
-        </button>
+        </Button>
+      }
+      {
+        <Button
+          variant="secondary"
+          disabled={login.length < 1 || password.length < 1 ? true : false}
+          onClick={() => onRegister(login, password)}
+        >
+          Register
+        </Button>
       }
     </div>
   );
 };
 
-const CLoginForm = connect(null, { onLogin: actionFullLogin })(LoginForm);
+const CLoginForm = connect(null, {
+  onLogin: actionFullLogin,
+  onRegister: actionFullRegister,
+})(LoginForm);
+
+const PageMain = () => <h1>Главная страница</h1>;
+
+const PageCategory = ({
+  match: {
+    params: { _id },
+  },
+  getData,
+  history,
+}) => {
+  useEffect(() => {
+    getData(_id);
+  }, [_id]);
+  return <CCategory />;
+};
+const CPageCategory = connect(null, { getData: actionCatById })(PageCategory);
+
+const Good = ({
+  good: { _id, name, description, price, images } = {},
+  onCartAdd,
+}) => {
+  return (
+    <div className="good">
+      <h1>Страница товара</h1>
+      <h1>{name}</h1>
+      {images && images[0] && images[0].url && (
+        <img src={backURL + "/" + images[0].url} />
+      )}
+      <p>
+        <strong>Описание:</strong> {description}
+      </p>
+      <p>
+        <strong>ID:</strong> {_id}
+      </p>
+      <strong>Цена: {price} USD</strong>
+      <br />
+      <Button
+        variant="success"
+        onClick={() => onCartAdd({ _id, name, price, images })}
+      >
+        Добавить в корзину
+      </Button>
+    </div>
+  );
+};
+const CGood = connect((state) => ({ good: state.promise.goodById?.payload }), {
+  onCartAdd: actionCartAdd,
+})(Good);
+
+const PageGood = ({
+  match: {
+    params: { _id },
+  },
+  getData,
+}) => {
+  useEffect(() => {
+    getData(_id);
+  }, [_id]);
+  return <CGood />;
+};
+const CPageGood = connect(null, { getData: actionGoodById })(PageGood);
+
+const Authorizations = ({ auth, actionLogOut }) => {
+  if (auth.payload) {
+    return (
+      <div>
+        <strong>Привет, {auth.payload.sub.login}</strong>
+        <Button variant="danger" onClick={() => actionLogOut()}>
+          Выйти
+        </Button>
+      </div>
+    );
+  } else {
+    return (
+      <Link className="nav-link" to={`/authorizations`}>
+        Войти/Зарегистрироваться
+      </Link>
+    );
+  }
+};
+
+const СAuthorizations = connect((state) => ({ auth: state.auth }), {
+  actionLogOut: actionAuthLogout,
+})(Authorizations);
+
+const Page404 = () => (
+  <>
+    <h1>404 Error Page</h1>
+    <p className="zoom-area">Page was not found</p>
+    <section className="error-container">
+      <span className="four">
+        <span className="screen-reader-text">4</span>
+      </span>
+      <span className="zero">
+        <span className="screen-reader-text">0</span>
+      </span>
+      <span className="four">
+        <span className="screen-reader-text">4</span>
+      </span>
+    </section>
+    <div className="link-container">
+      <Link to="/" className="more-link">
+        Go to home
+      </Link>
+    </div>
+  </>
+);
 
 const Main = () => (
   <main>
-    <Aside />
     <Content>
-      <CCategory />
+      <Switch>
+        <Route path="/" component={withRouter(PageMain)} exact />
+        <Route path="/category/:_id" component={withRouter(CPageCategory)} />
+        <Route path="/cart" component={withRouter(CCartPage)} />
+        <Route path="/good/:_id" component={withRouter(CPageGood)} />
+        <Route path="/authorizations" component={withRouter(CLoginForm)} />
+        <Route path="" component={withRouter(Page404)} />
+      </Switch>
     </Content>
   </main>
 );
 
-// const JSONTest = ({ data }) => (
-//   <pre>
-//     {JSON.stringify(data, null, 4)}
-//     {Math.random() > 0.5 && <h1>Hello</h1>}
-//   </pre>
-// );
-
-// const ReduxJSON = connect((state) => ({ data: state }))(JSONTest);
-
-const ListItem = ({ item }) => <li>{item}</li>;
-
-const List = ({ data = ["пиво", "чипсы", "сиги"] }) => (
-  <ul>
-    {data.map((item) => (
-      <ListItem item={item} />
-    ))}
-  </ul>
-);
-
-const LowerCase = ({ children }) => <>{children.toLowerCase()}</>;
-
-const Input = () => {
-  const [text, setText] = useState("text");
-  // text !== "Another text" && setTimeout(() => setText("Another text"), 2000);
-  return (
-    <>
-      <h1>{text}</h1>
-      <h1>
-        <LowerCase>{text}</LowerCase>
-      </h1>
-      <h1>Length: {text.length}</h1>
-      <input value={text} onChange={(e) => setText(e.target.value)} />
-    </>
-  );
-};
-
-const RGBInput = () => {
-  const [red, setRed] = useState(0);
-  const [green, setGreen] = useState(0);
-  const [blue, setBlue] = useState(0);
-  const color = `rgba(${red},${green},${blue},1)`;
-  const bounds = (x) => (x < 0 ? 0 : x > 255 ? 255 : x);
-  return (
-    <div style={{ backgroundColor: color }}>
-      <input
-        type="number"
-        min="0"
-        max="255"
-        value={red}
-        onChange={(e) => setRed(bounds(+e.target.value))}
-      />
-      <input
-        type="number"
-        min="0"
-        max="255"
-        value={green}
-        onChange={(e) => setGreen(bounds(+e.target.value))}
-      />
-      <input
-        type="number"
-        min="0"
-        max="255"
-        value={blue}
-        onChange={(e) => setBlue(bounds(+e.target.value))}
-      />
-    </div>
-  );
-};
+const history = createBrowserHistory();
 
 function App() {
   return (
-    <Provider store={store}>
-      <div className="App">
-        <CLoginForm />
-        <RGBInput />
-        <Header />
-        <Main />
-        <CCartPage />
-        <Footer />
-      </div>
-    </Provider>
+    <Router history={history}>
+      <Provider store={store}>
+        <div className="App">
+          <Header />
+          <Main />
+          <Footer />
+        </div>
+      </Provider>
+    </Router>
   );
 }
 

+ 226 - 5
1/src/App.scss

@@ -1,8 +1,6 @@
-.Logo {
-  img {
-    max-height: 100px;
-  }
-}
+@import "~bootstrap/scss/bootstrap";
+@import url("https://fonts.googleapis.com/css?family=Montserrat:400,600,700");
+@import url("https://fonts.googleapis.com/css?family=Catamaran:400,800");
 
 .GoodCard {
   border: 1px solid cyan;
@@ -20,6 +18,20 @@ App {
       }
     }
   }
+  main {
+    display: flex;
+    .Content {
+      justify-content: center;
+      align-items: center;
+      .LoginForm {
+        min-height: 100%;
+        font-family: "lato", sans-serif;
+        color: #fff;
+        background: #222222;
+      }
+    }
+  }
+
   footer {
     background-color: #303030;
     .Logo {
@@ -51,3 +63,212 @@ App {
     max-width: 40vh;
   }
 }
+
+.error-container {
+  text-align: center;
+  font-size: 106px;
+  font-family: "Catamaran", sans-serif;
+  font-weight: 800;
+  margin: 70px 15px;
+}
+.error-container > span {
+  display: inline-block;
+  position: relative;
+}
+.error-container > span.four {
+  width: 136px;
+  height: 43px;
+  border-radius: 999px;
+  background: linear-gradient(
+      140deg,
+      rgba(0, 0, 0, 0.1) 0%,
+      rgba(0, 0, 0, 0.07) 43%,
+      transparent 44%,
+      transparent 100%
+    ),
+    linear-gradient(
+      105deg,
+      transparent 0%,
+      transparent 40%,
+      rgba(0, 0, 0, 0.06) 41%,
+      rgba(0, 0, 0, 0.07) 76%,
+      transparent 77%,
+      transparent 100%
+    ),
+    linear-gradient(to right, #d89ca4, #e27b7e);
+}
+.error-container > span.four:before,
+.error-container > span.four:after {
+  content: "";
+  display: block;
+  position: absolute;
+  border-radius: 999px;
+}
+.error-container > span.four:before {
+  width: 43px;
+  height: 156px;
+  left: 60px;
+  bottom: -43px;
+  background: linear-gradient(
+      128deg,
+      rgba(0, 0, 0, 0.1) 0%,
+      rgba(0, 0, 0, 0.07) 40%,
+      transparent 41%,
+      transparent 100%
+    ),
+    linear-gradient(
+      116deg,
+      rgba(0, 0, 0, 0.1) 0%,
+      rgba(0, 0, 0, 0.07) 50%,
+      transparent 51%,
+      transparent 100%
+    ),
+    linear-gradient(to top, #99749d, #b895ab, #cc9aa6, #d7969e, #e0787f);
+}
+.error-container > span.four:after {
+  width: 137px;
+  height: 43px;
+  transform: rotate(-49.5deg);
+  left: -18px;
+  bottom: 36px;
+  background: linear-gradient(
+    to right,
+    #99749d,
+    #b895ab,
+    #cc9aa6,
+    #d7969e,
+    #e0787f
+  );
+}
+
+.error-container > span.zero {
+  vertical-align: text-top;
+  width: 156px;
+  height: 156px;
+  border-radius: 999px;
+  background: linear-gradient(
+      -45deg,
+      transparent 0%,
+      rgba(0, 0, 0, 0.06) 50%,
+      transparent 51%,
+      transparent 100%
+    ),
+    linear-gradient(
+      to top right,
+      #99749d,
+      #99749d,
+      #b895ab,
+      #cc9aa6,
+      #d7969e,
+      #ed8687,
+      #ed8687
+    );
+  overflow: hidden;
+  animation: bgshadow 5s infinite;
+}
+.error-container > span.zero:before {
+  content: "";
+  display: block;
+  position: absolute;
+  transform: rotate(45deg);
+  width: 90px;
+  height: 90px;
+  background-color: transparent;
+  left: 0px;
+  bottom: 0px;
+  background: linear-gradient(
+      95deg,
+      transparent 0%,
+      transparent 8%,
+      rgba(0, 0, 0, 0.07) 9%,
+      transparent 50%,
+      transparent 100%
+    ),
+    linear-gradient(
+      85deg,
+      transparent 0%,
+      transparent 19%,
+      rgba(0, 0, 0, 0.05) 20%,
+      rgba(0, 0, 0, 0.07) 91%,
+      transparent 92%,
+      transparent 100%
+    );
+}
+.error-container > span.zero:after {
+  content: "";
+  display: block;
+  position: absolute;
+  border-radius: 999px;
+  width: 70px;
+  height: 70px;
+  left: 43px;
+  bottom: 43px;
+  background: #fdfaf5;
+  box-shadow: -2px 2px 2px 0px rgba(0, 0, 0, 0.1);
+}
+
+.screen-reader-text {
+  position: absolute;
+  top: -9999em;
+  left: -9999em;
+}
+
+@keyframes bgshadow {
+  0% {
+    box-shadow: inset -160px 160px 0px 5px rgba(0, 0, 0, 0.4);
+  }
+  45% {
+    box-shadow: inset 0px 0px 0px 0px rgba(0, 0, 0, 0.1);
+  }
+  55% {
+    box-shadow: inset 0px 0px 0px 0px rgba(0, 0, 0, 0.1);
+  }
+  100% {
+    box-shadow: inset 160px -160px 0px 5px rgba(0, 0, 0, 0.4);
+  }
+}
+* {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+body {
+  background-color: #fdfaf5;
+  margin-bottom: 50px;
+}
+html,
+button,
+input,
+select,
+textarea {
+  font-family: "Montserrat", Helvetica, sans-serif;
+  color: #bbb;
+}
+h1 {
+  text-align: center;
+  margin: 30px 15px;
+}
+.zoom-area {
+  max-width: 490px;
+  margin: 30px auto 30px;
+  font-size: 19px;
+  text-align: center;
+}
+.link-container {
+  text-align: center;
+}
+a.more-link {
+  text-transform: uppercase;
+  font-size: 13px;
+  background-color: #de7e85;
+  padding: 10px 15px;
+  border-radius: 0;
+  color: #fff;
+  display: inline-block;
+  margin-right: 5px;
+  margin-bottom: 5px;
+  line-height: 1.5;
+  text-decoration: none;
+  margin-top: 50px;
+  letter-spacing: 1px;
+}

+ 180 - 0
2/package-lock.json

@@ -1842,6 +1842,44 @@
         }
       }
     },
+    "@popperjs/core": {
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz",
+      "integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ=="
+    },
+    "@react-aria/ssr": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.1.0.tgz",
+      "integrity": "sha512-RxqQKmE8sO7TGdrcSlHTcVzMP450hqowtBSd2bBS9oPlcokVkaGq28c3Rwa8ty5ctw4EBCjXqjP7xdcKMGDzug==",
+      "requires": {
+        "@babel/runtime": "^7.6.2"
+      }
+    },
+    "@restart/hooks": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.5.tgz",
+      "integrity": "sha512-tLGtY0aHeIfT7aPwUkvQuhIy3+q3w4iqmUzFLPlOAf/vNUacLaBt1j/S//jv/dQhenRh8jvswyMojCwmLvJw8A==",
+      "requires": {
+        "dequal": "^2.0.2"
+      }
+    },
+    "@restart/ui": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-0.2.5.tgz",
+      "integrity": "sha512-3dP8pMFickPpvAG5MVQW53HnJl0c17h7MwvI4nNy9QF66sHSYVchudlqlI8eOSaqnmc5YVjGura63vMb9LTNbQ==",
+      "requires": {
+        "@babel/runtime": "^7.13.16",
+        "@popperjs/core": "^2.10.1",
+        "@react-aria/ssr": "^3.0.1",
+        "@restart/hooks": "^0.4.0",
+        "@types/warning": "^3.0.0",
+        "dequal": "^2.0.2",
+        "dom-helpers": "^5.2.0",
+        "prop-types": "^15.7.2",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      }
+    },
     "@rollup/plugin-babel": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz",
@@ -2304,6 +2342,11 @@
         "@types/node": "*"
       }
     },
+    "@types/invariant": {
+      "version": "2.2.35",
+      "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
+      "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg=="
+    },
     "@types/istanbul-lib-coverage": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -2369,11 +2412,34 @@
       "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.2.tgz",
       "integrity": "sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA=="
     },
+    "@types/prop-types": {
+      "version": "15.7.4",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
+      "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
+    },
     "@types/q": {
       "version": "1.5.5",
       "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz",
       "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ=="
     },
+    "@types/react": {
+      "version": "17.0.38",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.38.tgz",
+      "integrity": "sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==",
+      "requires": {
+        "@types/prop-types": "*",
+        "@types/scheduler": "*",
+        "csstype": "^3.0.2"
+      }
+    },
+    "@types/react-transition-group": {
+      "version": "4.4.4",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
+      "integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==",
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/resolve": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -2387,6 +2453,11 @@
       "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz",
       "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g=="
     },
+    "@types/scheduler": {
+      "version": "0.16.2",
+      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+      "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+    },
     "@types/stack-utils": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -2405,6 +2476,11 @@
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
       "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
     },
+    "@types/warning": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
+      "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
+    },
     "@types/yargs": {
       "version": "16.0.4",
       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@@ -3379,6 +3455,11 @@
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
       "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
     },
+    "bootstrap": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
+      "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q=="
+    },
     "brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -3594,6 +3675,11 @@
       "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
       "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
     },
+    "classnames": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+      "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+    },
     "clean-css": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz",
@@ -4128,6 +4214,11 @@
         }
       }
     },
+    "csstype": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
+      "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
+    },
     "damerau-levenshtein": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz",
@@ -4274,6 +4365,11 @@
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
       "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
     },
+    "dequal": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
+      "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
+    },
     "destroy": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
@@ -4389,6 +4485,15 @@
         "utila": "~0.4"
       }
     },
+    "dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "requires": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "dom-serializer": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
@@ -6242,6 +6347,14 @@
         "side-channel": "^1.0.4"
       }
     },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "ip": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -9765,6 +9878,15 @@
         "react-is": "^16.8.1"
       }
     },
+    "prop-types-extra": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
+      "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
+      "requires": {
+        "react-is": "^16.3.2",
+        "warning": "^4.0.0"
+      }
+    },
     "proxy-addr": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -9877,6 +9999,29 @@
         "whatwg-fetch": "^3.6.2"
       }
     },
+    "react-bootstrap": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.0.4.tgz",
+      "integrity": "sha512-sfxhLKY/P5oeqkcxI4Q3SkJKLBq/7pv1wGykGlmCLWp4Pll3HMVl5VRtVbKsE4FzGsZGhXXauhi2HhRbmWLwBA==",
+      "requires": {
+        "@babel/runtime": "^7.14.0",
+        "@restart/hooks": "^0.4.5",
+        "@restart/ui": "^0.2.5",
+        "@types/invariant": "^2.2.33",
+        "@types/prop-types": "^15.7.3",
+        "@types/react": ">=16.14.8",
+        "@types/react-transition-group": "^4.4.1",
+        "@types/warning": "^3.0.0",
+        "classnames": "^2.3.1",
+        "dom-helpers": "^5.2.1",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.7.2",
+        "prop-types-extra": "^1.1.0",
+        "react-transition-group": "^4.4.1",
+        "uncontrollable": "^7.2.1",
+        "warning": "^4.0.3"
+      }
+    },
     "react-dev-utils": {
       "version": "12.0.0",
       "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
@@ -9983,6 +10128,11 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
+    "react-lifecycles-compat": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+      "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
+    },
     "react-refresh": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -10043,6 +10193,17 @@
         "workbox-webpack-plugin": "^6.4.1"
       }
     },
+    "react-transition-group": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
+      "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
+      "requires": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      }
+    },
     "read-pkg": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -11712,6 +11873,17 @@
         "which-boxed-primitive": "^1.0.2"
       }
     },
+    "uncontrollable": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
+      "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
+      "requires": {
+        "@babel/runtime": "^7.6.3",
+        "@types/react": ">=16.9.11",
+        "invariant": "^2.2.4",
+        "react-lifecycles-compat": "^3.0.4"
+      }
+    },
     "unicode-canonical-property-names-ecmascript": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@@ -11896,6 +12068,14 @@
         "makeerror": "1.0.12"
       }
     },
+    "warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "watchpack": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",

+ 2 - 0
2/package.json

@@ -6,9 +6,11 @@
     "@testing-library/jest-dom": "^5.16.1",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "bootstrap": "^5.1.3",
     "node-sass": "^7.0.0",
     "node-scss": "^7.0.3",
     "react": "^17.0.2",
+    "react-bootstrap": "^2.0.4",
     "react-dom": "^17.0.2",
     "react-scripts": "5.0.0",
     "web-vitals": "^2.1.2"

+ 629 - 4
2/src/App.js

@@ -1,15 +1,66 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
 import "./App.scss";
+import watch from "./images/ClockFace.png";
+import secondArrow from "./images/ClockFace_S.png";
+import minuteArrow from "./images/ClockFace_M.png";
+import hourArrow from "./images/ClockFace_H.png";
+import logoDefault from "./logo.svg";
+import { Button } from "react-bootstrap";
+
+// RGBInput
+const RGBInput = () => {
+  const [red, setRed] = useState(0);
+  const [green, setGreen] = useState(0);
+  const [blue, setBlue] = useState(0);
+  const color = `rgba(${red},${green},${blue},1)`;
+
+  useEffect(() => {
+    console.log("component did mount");
+    return () => {
+      console.log("component will unmount");
+    };
+  }, [red]);
+
+  const bounds = (x) => (x < 0 ? 0 : x > 255 ? 255 : x);
+  return (
+    <div style={{ backgroundColor: color }}>
+      <input
+        type="number"
+        min="0"
+        max="255"
+        value={red}
+        onChange={(e) => setRed(bounds(+e.target.value))}
+      />
+      <input
+        type="number"
+        min="0"
+        max="255"
+        value={green}
+        onChange={(e) => setGreen(bounds(+e.target.value))}
+      />
+      <input
+        type="number"
+        min="0"
+        max="255"
+        value={blue}
+        onChange={(e) => setBlue(bounds(+e.target.value))}
+      />
+    </div>
+  );
+};
 
 // Spoiler
 const Spoiler = ({ header = <h4>+</h4>, open, children }) => {
   const [visible, setVisible] = useState(open);
   return (
     <div className="Spoiler">
-      <div className="header" onClick={() => setVisible(!visible)}>
-        {header}
+      <div className="header" onClick={(e) => setVisible(!visible)}>
+        <h3>
+          {header}
+          {visible ? "hide" : "show"}
+        </h3>
+        {visible && children}
       </div>
-      <div className={visible ? "visible" : "hidden"}>{children}</div>
     </div>
   );
 };
@@ -75,9 +126,554 @@ const PasswordConfirm = ({ min = 4 }) => {
   );
 };
 
+// Timers
+
+const Timer = ({
+  ms = 1000,
+  onDelete = null,
+  h = null,
+  m = null,
+  s = null,
+}) => {
+  const [counter, setCounter] = useState(0);
+  const [pause, setPause] = useState(false);
+
+  // counter <= 0 ? setPause(true) : null;
+  useEffect(() => {
+    const interval = setInterval(() => {
+      if (!pause) {
+        setCounter((counter) => counter + 1);
+      }
+    }, ms);
+    return () => clearInterval(interval);
+  }, [pause, ms]);
+
+  let seconds = s ? s + counter / 1000 : counter / 1000;
+  let minutes = m ? m + seconds / 60 : seconds / 60;
+  let hours = h ? h + minutes / 60 : minutes / 60;
+
+  return (
+    <>
+      <h3>
+        Hours: {hours.toFixed(3)} | Minutes: {minutes.toFixed(3)} | Seconds:{" "}
+        {seconds.toFixed(3)}
+      </h3>
+      {onDelete && <button onClick={onDelete}>x</button>}
+      <button
+        onClick={() => {
+          setPause(!pause);
+        }}
+      >
+        {pause ? "Continue" : "Pause"}
+      </button>
+    </>
+  );
+};
+
+const Timers = () => {
+  const [timers, setTimers] = useState([]);
+  const [ms, setMS] = useState(1000);
+  return (
+    <>
+      <button onClick={() => setMS(ms + 100)}>+</button>
+      {ms}
+      <button onClick={() => setMS(ms - 100)}>-</button>
+      <button onClick={() => setTimers([Math.random(), ...timers])}>ADD</button>
+      {timers.map((i) => (
+        <Timer
+          key={i}
+          ms={ms}
+          onDelete={() => setTimers(timers.filter((t) => t !== i))}
+        />
+      ))}
+    </>
+  );
+};
+
+const TimerControl = () => {
+  const [seconds, setSeconds] = useState(0);
+  const [minutes, setMinutes] = useState(0);
+  const [hours, setHours] = useState(0);
+  const [start, setStart] = useState(false);
+
+  return (
+    <div className="TimerControl">
+      <label htmlFor="Hours">{hours} hours</label>
+      <input
+        id="Hours"
+        min="0"
+        type="number"
+        onChange={(e) =>
+          setHours(+e.currentTarget.value ? +e.currentTarget.value : 0)
+        }
+      />{" "}
+      <label htmlFor="Minutes">{minutes} minutes</label>
+      <input
+        id="Minutes"
+        min="0"
+        type="number"
+        onChange={(e) =>
+          setMinutes(+e.currentTarget.value ? +e.currentTarget.value : 0)
+        }
+      />{" "}
+      <label htmlFor="Seconds">{seconds} seconds</label>
+      <input
+        id="Seconds"
+        min="0"
+        type="number"
+        onChange={(e) =>
+          setSeconds(+e.currentTarget.value ? +e.currentTarget.value : 0)
+        }
+      />{" "}
+      <button onClick={() => setStart(!start)}>
+        {start ? "Hide" : "Start"}
+      </button>
+      {start && <Timer h={hours} m={minutes} s={seconds} />}
+    </div>
+  );
+};
+
+const TimerContainer = ({
+  seconds,
+  refresh,
+  render: Render,
+  mode = "minus",
+}) => {
+  const firstTimer = performance.now();
+  const [time, setTime] = useState(seconds);
+  useEffect(() => {
+    const interval = setInterval(() => {
+      let secondTimer = performance.now();
+      let timerSeconds = (secondTimer - firstTimer) / 1000;
+      if (mode === "minus") {
+        if (seconds >= timerSeconds) {
+          setTime(seconds - timerSeconds);
+        } else {
+          setTime(0);
+          clearInterval(interval);
+        }
+      } else {
+        setTime(seconds + timerSeconds);
+      }
+    }, refresh);
+    return () => {
+      setTime(seconds);
+      clearInterval(interval);
+    };
+  }, [seconds]);
+  return <Render seconds={time.toFixed(1)} />;
+};
+const SecondsTimer = ({ seconds }) => <h2>{seconds}</h2>;
+
+const TimerLCD = ({ seconds = 3600 }) => {
+  let sec = seconds * 1;
+  let minutes = seconds / 60;
+  let hours = minutes / 60;
+  return (
+    <h4>
+      Hours: {hours.toFixed(1)} | Minutes: {minutes.toFixed(1)} | Seconds:{" "}
+      {sec.toFixed(1)}
+    </h4>
+  );
+};
+
+const Watch = ({ seconds }) => {
+  let ms = seconds * 1000;
+  let secondAngle = ms * 0.006;
+  let minuteAngle = ms * 0.0001;
+  let hourAngle = ms * 0.00000166666;
+
+  return (
+    <div className="Watch">
+      <img src={watch} alt="Watch" />
+      <img
+        className="Arrows"
+        style={{ transform: `rotate(${secondAngle}deg)` }}
+        src={secondArrow}
+        alt="Seconds"
+      />
+      <img
+        className="Arrows"
+        style={{ transform: `rotate(${minuteAngle}deg)` }}
+        src={minuteArrow}
+        alt="Minutes"
+      />
+      <img
+        className="Arrows"
+        style={{ transform: `rotate(${hourAngle}deg)` }}
+        src={hourArrow}
+        alt="Hours"
+      />
+    </div>
+  );
+};
+
+const TimerControlContainer = ({ refresh, render: Render }) => {
+  const firstTimer = performance.now();
+  const [seconds, setSeconds] = useState(0);
+  const [start, setStart] = useState(false);
+  const [time, setTime] = useState(seconds);
+
+  useEffect(() => {
+    const interval = setInterval(() => {
+      let secondTimer = performance.now();
+      let timerSeconds = (secondTimer - firstTimer) / 1000;
+      if (seconds >= timerSeconds) {
+        setTime(seconds - timerSeconds);
+      } else {
+        setTime(0);
+        clearInterval(interval);
+      }
+    }, refresh);
+    return () => {
+      setTime(seconds);
+      clearInterval(interval);
+    };
+  }, [seconds]);
+
+  return (
+    <div className="TimerControl">
+      <label for="Seconds">{seconds} seconds</label>
+      <input
+        id="Seconds"
+        min="0"
+        type="number"
+        onChange={(e) =>
+          setSeconds(+e.currentTarget.value ? +e.currentTarget.value : 0)
+        }
+      />{" "}
+      <button onClick={() => setStart(!start)}>
+        {start ? "Hide" : "Start"}
+      </button>
+      {start && <Render seconds={time.toFixed(1)} />}
+    </div>
+  );
+};
+
+// MySelect
+const countries = {
+  AF: "Afghanistan",
+  AX: "Aland Islands",
+  AL: "Albania",
+  DZ: "Algeria",
+  AS: "American Samoa",
+  AD: "Andorra",
+  AO: "Angola",
+  AI: "Anguilla",
+  AQ: "Antarctica",
+  AG: "Antigua And Barbuda",
+  AR: "Argentina",
+  AM: "Armenia",
+  AW: "Aruba",
+  AU: "Australia",
+  AT: "Austria",
+  AZ: "Azerbaijan",
+  BS: "Bahamas",
+  BH: "Bahrain",
+  BD: "Bangladesh",
+  BB: "Barbados",
+  BY: "Belarus",
+  BE: "Belgium",
+  BZ: "Belize",
+  BJ: "Benin",
+  BM: "Bermuda",
+  BT: "Bhutan",
+  BO: "Bolivia",
+  BA: "Bosnia And Herzegovina",
+  BW: "Botswana",
+  BV: "Bouvet Island",
+  BR: "Brazil",
+  IO: "British Indian Ocean Territory",
+  BN: "Brunei Darussalam",
+  BG: "Bulgaria",
+  BF: "Burkina Faso",
+  BI: "Burundi",
+  KH: "Cambodia",
+  CM: "Cameroon",
+  CA: "Canada",
+  CV: "Cape Verde",
+  KY: "Cayman Islands",
+  CF: "Central African Republic",
+  TD: "Chad",
+  CL: "Chile",
+  CN: "China",
+  CX: "Christmas Island",
+  CC: "Cocos (Keeling) Islands",
+  CO: "Colombia",
+  KM: "Comoros",
+  CG: "Congo",
+  CD: "Congo, Democratic Republic",
+  CK: "Cook Islands",
+  CR: "Costa Rica",
+  CI: 'Cote D"Ivoire',
+  HR: "Croatia",
+  CU: "Cuba",
+  CY: "Cyprus",
+  CZ: "Czech Republic",
+  DK: "Denmark",
+  DJ: "Djibouti",
+  DM: "Dominica",
+  DO: "Dominican Republic",
+  EC: "Ecuador",
+  EG: "Egypt",
+  SV: "El Salvador",
+  GQ: "Equatorial Guinea",
+  ER: "Eritrea",
+  EE: "Estonia",
+  ET: "Ethiopia",
+  FK: "Falkland Islands (Malvinas)",
+  FO: "Faroe Islands",
+  FJ: "Fiji",
+  FI: "Finland",
+  FR: "France",
+  GF: "French Guiana",
+  PF: "French Polynesia",
+  TF: "French Southern Territories",
+  GA: "Gabon",
+  GM: "Gambia",
+  GE: "Georgia",
+  DE: "Germany",
+  GH: "Ghana",
+  GI: "Gibraltar",
+  GR: "Greece",
+  GL: "Greenland",
+  GD: "Grenada",
+  GP: "Guadeloupe",
+  GU: "Guam",
+  GT: "Guatemala",
+  GG: "Guernsey",
+  GN: "Guinea",
+  GW: "Guinea-Bissau",
+  GY: "Guyana",
+  HT: "Haiti",
+  HM: "Heard Island & Mcdonald Islands",
+  VA: "Holy See (Vatican City State)",
+  HN: "Honduras",
+  HK: "Hong Kong",
+  HU: "Hungary",
+  IS: "Iceland",
+  IN: "India",
+  ID: "Indonesia",
+  IR: "Iran, Islamic Republic Of",
+  IQ: "Iraq",
+  IE: "Ireland",
+  IM: "Isle Of Man",
+  IL: "Israel",
+  IT: "Italy",
+  JM: "Jamaica",
+  JP: "Japan",
+  JE: "Jersey",
+  JO: "Jordan",
+  KZ: "Kazakhstan",
+  KE: "Kenya",
+  KI: "Kiribati",
+  KR: "Korea",
+  KP: "North Korea",
+  KW: "Kuwait",
+  KG: "Kyrgyzstan",
+  LA: 'Lao People"s Democratic Republic',
+  LV: "Latvia",
+  LB: "Lebanon",
+  LS: "Lesotho",
+  LR: "Liberia",
+  LY: "Libyan Arab Jamahiriya",
+  LI: "Liechtenstein",
+  LT: "Lithuania",
+  LU: "Luxembourg",
+  MO: "Macao",
+  MK: "Macedonia",
+  MG: "Madagascar",
+  MW: "Malawi",
+  MY: "Malaysia",
+  MV: "Maldives",
+  ML: "Mali",
+  MT: "Malta",
+  MH: "Marshall Islands",
+  MQ: "Martinique",
+  MR: "Mauritania",
+  MU: "Mauritius",
+  YT: "Mayotte",
+  MX: "Mexico",
+  FM: "Micronesia, Federated States Of",
+  MD: "Moldova",
+  MC: "Monaco",
+  MN: "Mongolia",
+  ME: "Montenegro",
+  MS: "Montserrat",
+  MA: "Morocco",
+  MZ: "Mozambique",
+  MM: "Myanmar",
+  NA: "Namibia",
+  NR: "Nauru",
+  NP: "Nepal",
+  NL: "Netherlands",
+  AN: "Netherlands Antilles",
+  NC: "New Caledonia",
+  NZ: "New Zealand",
+  NI: "Nicaragua",
+  NE: "Niger",
+  NG: "Nigeria",
+  NU: "Niue",
+  NF: "Norfolk Island",
+  MP: "Northern Mariana Islands",
+  NO: "Norway",
+  OM: "Oman",
+  PK: "Pakistan",
+  PW: "Palau",
+  PS: "Palestinian Territory, Occupied",
+  PA: "Panama",
+  PG: "Papua New Guinea",
+  PY: "Paraguay",
+  PE: "Peru",
+  PH: "Philippines",
+  PN: "Pitcairn",
+  PL: "Poland",
+  PT: "Portugal",
+  PR: "Puerto Rico",
+  QA: "Qatar",
+  RE: "Reunion",
+  RO: "Romania",
+  RU: "Russian Federation",
+  RW: "Rwanda",
+  BL: "Saint Barthelemy",
+  SH: "Saint Helena",
+  KN: "Saint Kitts And Nevis",
+  LC: "Saint Lucia",
+  MF: "Saint Martin",
+  PM: "Saint Pierre And Miquelon",
+  VC: "Saint Vincent And Grenadines",
+  WS: "Samoa",
+  SM: "San Marino",
+  ST: "Sao Tome And Principe",
+  SA: "Saudi Arabia",
+  SN: "Senegal",
+  RS: "Serbia",
+  SC: "Seychelles",
+  SL: "Sierra Leone",
+  SG: "Singapore",
+  SK: "Slovakia",
+  SI: "Slovenia",
+  SB: "Solomon Islands",
+  SO: "Somalia",
+  ZA: "South Africa",
+  GS: "South Georgia And Sandwich Isl.",
+  ES: "Spain",
+  LK: "Sri Lanka",
+  SD: "Sudan",
+  SR: "Suriname",
+  SJ: "Svalbard And Jan Mayen",
+  SZ: "Swaziland",
+  SE: "Sweden",
+  CH: "Switzerland",
+  SY: "Syrian Arab Republic",
+  TW: "Taiwan",
+  TJ: "Tajikistan",
+  TZ: "Tanzania",
+  TH: "Thailand",
+  TL: "Timor-Leste",
+  TG: "Togo",
+  TK: "Tokelau",
+  TO: "Tonga",
+  TT: "Trinidad And Tobago",
+  TN: "Tunisia",
+  TR: "Turkey",
+  TM: "Turkmenistan",
+  TC: "Turks And Caicos Islands",
+  TV: "Tuvalu",
+  UG: "Uganda",
+  UA: "Ukraine",
+  AE: "United Arab Emirates",
+  GB: "United Kingdom",
+  US: "United States",
+  UM: "United States Outlying Islands",
+  UY: "Uruguay",
+  UZ: "Uzbekistan",
+  VU: "Vanuatu",
+  VE: "Venezuela",
+  VN: "Vietnam",
+  VG: "Virgin Islands, British",
+  VI: "Virgin Islands, U.S.",
+  WF: "Wallis And Futuna",
+  EH: "Western Sahara",
+  YE: "Yemen",
+  ZM: "Zambia",
+  ZW: "Zimbabwe",
+};
+
+const MySelect = ({ options = countries, value = "YE", onChange }) => (
+  <select value={value} onChange={(value) => onChange(value.target.value)}>
+    {Object.entries(options).map((option) => (
+      <option value={option[0]}>{option[1]}</option>
+    ))}
+  </select>
+);
+
+// Gallery
+const Dots = ({ active, count, onClick }) => {
+  const dots = [];
+  for (let i = 0; i < count; i++) {
+    dots.push(
+      <div
+        className="Dots"
+        style={
+          i === active
+            ? { border: "2px solid red" }
+            : { border: "2px solid black" }
+        }
+        onClick={() => onClick(i)}
+      >
+        {i + 1}
+      </div>
+    );
+  }
+  return <div className="Dots">{dots}</div>;
+};
+
+const Gallery = ({
+  images = [
+    logoDefault,
+    "https://go64.ru/upload/quickly/cat-2143332_1280.jpg",
+    "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hermitage_cat.jpeg/1200px-Hermitage_cat.jpeg",
+  ],
+}) => {
+  let [index, setIndex] = useState(0);
+  return (
+    <div className="Gallery">
+      <img src={images[index]} alt="myImage" />
+      <Button
+        variant="warning"
+        onClick={
+          index - 1 < 0
+            ? () => setIndex(images.length - 1)
+            : () => setIndex(index - 1)
+        }
+      >
+        Предыдущая
+      </Button>
+      <Button
+        variant="success"
+        onClick={
+          index + 1 === images.length
+            ? () => setIndex(0)
+            : () => setIndex(index + 1)
+        }
+      >
+        Следующая
+      </Button>
+      <br />
+      <Dots active={index} count={images.length} onClick={(i) => setIndex(i)} />
+    </div>
+  );
+};
+
 function App() {
+  const [country, setCountry] = useState("UA");
   return (
     <div className="App">
+      <h3>RGB Input:</h3>
+      <RGBInput />
+
+      <h3>Spoilers:</h3>
       <Spoiler header={<h1>Заголовок</h1>} open>
         <h2>Контент 1</h2>
         <p>
@@ -101,9 +697,38 @@ function App() {
         </p>
       </Spoiler>
 
+      <h3>RangeInput:</h3>
       <RangeInput min={2} max={10} />
 
+      <h3>PasswordConfirm:</h3>
       <PasswordConfirm min={4} />
+
+      <h3>Timers:</h3>
+      <Timers />
+
+      <h3>TimerControl:</h3>
+      <TimerControl />
+
+      <h3>TimerContainer:</h3>
+      <TimerContainer seconds={1000} refresh={100} render={SecondsTimer} />
+
+      <h3>LCD:</h3>
+      <TimerContainer seconds={3600} refresh={100} render={TimerLCD} />
+
+      <h3>TimerControlContainer:</h3>
+      <TimerControlContainer refresh={100} render={TimerLCD} />
+
+      <h3>Watch:</h3>
+      <TimerContainer seconds={10} refresh={100} render={Watch} mode="plus" />
+
+      <h3>MySelect:</h3>
+      <MySelect
+        value={country}
+        onChange={(newCountry) => setCountry(newCountry)}
+      />
+
+      <h3>Gallery:</h3>
+      <Gallery />
     </div>
   );
 }

+ 49 - 5
2/src/App.scss

@@ -1,3 +1,5 @@
+@import "~bootstrap/scss/bootstrap";
+
 .Spoiler {
   .header {
     border: 1px solid black;
@@ -10,14 +12,17 @@
       cursor: pointer;
     }
   }
-  .visible {
+}
+.RangeInput {
+  label {
     display: block;
   }
-  .hidden {
-    display: none;
+  input {
+    display: block;
+    outline: none;
   }
 }
-.RangeInput {
+.PasswordConfirm {
   label {
     display: block;
   }
@@ -26,7 +31,8 @@
     outline: none;
   }
 }
-.PasswordConfirm {
+
+.TimerControl {
   label {
     display: block;
   }
@@ -35,3 +41,41 @@
     outline: none;
   }
 }
+
+.Watch {
+  display: inline-block;
+  .Arrows {
+    left: 0;
+    position: absolute;
+  }
+}
+
+.Gallery {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  align-items: center;
+  img {
+    width: 40%;
+    height: 30%;
+  }
+  button {
+    width: 40%;
+    height: 40%;
+  }
+  .Dots {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 12em;
+    height: 3em;
+    cursor: pointer;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    -o-user-select: none;
+    -webkit-user-select: none;
+    border-radius: 50%;
+    text-align: center;
+    margin: 1vh;
+  }
+}

BIN
2/src/images/ClockFace.png


BIN
2/src/images/ClockFace_H.png


BIN
2/src/images/ClockFace_M.png


BIN
2/src/images/ClockFace_S.png