|
@@ -0,0 +1,634 @@
|
|
|
+/* eslint-disable jsx-a11y/alt-text */
|
|
|
+import React, { useState } from "react";
|
|
|
+import logoDefault from "./logo.svg";
|
|
|
+import "./App.scss";
|
|
|
+import { Provider, connect } from "react-redux";
|
|
|
+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";
|
|
|
+
|
|
|
+const jwtDecode = (token) => {
|
|
|
+ try {
|
|
|
+ let arrToken = token.split(".");
|
|
|
+ let base64Token = atob(arrToken[1]);
|
|
|
+ return JSON.parse(base64Token);
|
|
|
+ } catch (e) {
|
|
|
+ console.log("Error JWT: " + e);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const getGQL =
|
|
|
+ (url) =>
|
|
|
+ async (query, variables = {}) => {
|
|
|
+ let obj = await fetch(url, {
|
|
|
+ method: "POST",
|
|
|
+ headers: {
|
|
|
+ "Content-Type": "application/json",
|
|
|
+ Authorization: localStorage.authToken
|
|
|
+ ? "Bearer " + localStorage.authToken
|
|
|
+ : {},
|
|
|
+ },
|
|
|
+ body: JSON.stringify({ query, variables }),
|
|
|
+ });
|
|
|
+ let a = await obj.json();
|
|
|
+ if (!a.data && a.errors) throw new Error(JSON.stringify(a.errors));
|
|
|
+ return a.data[Object.keys(a.data)[0]];
|
|
|
+ };
|
|
|
+
|
|
|
+const backURL = "http://shop-roles.asmer.fs.a-level.com.ua";
|
|
|
+
|
|
|
+const gql = getGQL(backURL + "/graphql");
|
|
|
+
|
|
|
+const actionRootCats = () =>
|
|
|
+ actionPromise(
|
|
|
+ "rootCats",
|
|
|
+ gql(`query {
|
|
|
+ CategoryFind(query: "[{\\"parent\\":null}]"){
|
|
|
+ _id name
|
|
|
+ }
|
|
|
+ }`)
|
|
|
+ );
|
|
|
+
|
|
|
+const actionCatById = (_id) =>
|
|
|
+ actionPromise(
|
|
|
+ "catById",
|
|
|
+ gql(
|
|
|
+ `query catById($q: String){
|
|
|
+ CategoryFindOne(query: $q){
|
|
|
+ subCategories{name, _id}
|
|
|
+ _id name goods {
|
|
|
+ _id name price images {
|
|
|
+ url
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }`,
|
|
|
+ { q: JSON.stringify([{ _id }]) }
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+const actionPromise = (name, promise) => async (dispatch) => {
|
|
|
+ dispatch(actionPending(name));
|
|
|
+ try {
|
|
|
+ let data = await promise;
|
|
|
+ dispatch(actionResolved(name, data));
|
|
|
+ return data;
|
|
|
+ } catch (error) {
|
|
|
+ dispatch(actionRejected(name, error));
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const actionPending = (name) => ({ type: "PROMISE", status: "PENDING", name });
|
|
|
+const actionResolved = (name, payload) => ({
|
|
|
+ type: "PROMISE",
|
|
|
+ status: "RESOLVED",
|
|
|
+ name,
|
|
|
+ payload,
|
|
|
+});
|
|
|
+const actionRejected = (name, error) => ({
|
|
|
+ type: "PROMISE",
|
|
|
+ status: "REJECTED",
|
|
|
+ name,
|
|
|
+ error,
|
|
|
+});
|
|
|
+
|
|
|
+const actionLogin = (login, password) =>
|
|
|
+ actionPromise(
|
|
|
+ "login",
|
|
|
+ gql(
|
|
|
+ `query NameForMe1($login:String, $password:String){
|
|
|
+ login(login:$login, password:$password)
|
|
|
+ }`,
|
|
|
+ { login, 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 actionAuthLogin = (token) => ({ type: "AUTH_LOGIN", token });
|
|
|
+const actionAuthLogout = () => ({ type: "AUTH_LOGOUT" });
|
|
|
+
|
|
|
+function cartReducer(state = {}, { type, good = {}, count = 1 }) {
|
|
|
+ const { _id } = good;
|
|
|
+ const types = {
|
|
|
+ CART_ADD() {
|
|
|
+ count = +count;
|
|
|
+ if (!count) return state;
|
|
|
+ return {
|
|
|
+ ...state,
|
|
|
+ [_id]: {
|
|
|
+ good,
|
|
|
+ count: count + (state[_id]?.count || 0),
|
|
|
+ },
|
|
|
+ };
|
|
|
+ },
|
|
|
+ CART_CHANGE() {
|
|
|
+ count = +count;
|
|
|
+ if (!count) return state;
|
|
|
+ return {
|
|
|
+ ...state,
|
|
|
+ [_id]: {
|
|
|
+ good,
|
|
|
+ count: count,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ },
|
|
|
+ CART_REMOVE() {
|
|
|
+ let { [_id]: remove, ...newState } = state;
|
|
|
+ return {
|
|
|
+ ...newState,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ CART_CLEAR() {
|
|
|
+ return {};
|
|
|
+ },
|
|
|
+ };
|
|
|
+ if (type in types) {
|
|
|
+ return types[type]();
|
|
|
+ }
|
|
|
+ return state;
|
|
|
+}
|
|
|
+
|
|
|
+const actionCartAdd = (good, count = 1) => ({
|
|
|
+ type: "CART_ADD",
|
|
|
+ good,
|
|
|
+ count,
|
|
|
+});
|
|
|
+
|
|
|
+const actionCartChange = (good, count) => ({
|
|
|
+ type: "CART_CHANGE",
|
|
|
+ good,
|
|
|
+ count,
|
|
|
+});
|
|
|
+
|
|
|
+const actionCartRemove = (good) => ({ type: "CART_REMOVE", good });
|
|
|
+
|
|
|
+const actionCartClear = () => ({ type: "CART_CLEAR" });
|
|
|
+
|
|
|
+function promiseReducer(state = {}, { type, status, payload, error, name }) {
|
|
|
+ if (type === "PROMISE") {
|
|
|
+ return {
|
|
|
+ ...state,
|
|
|
+ [name]: { status, payload, error },
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return state;
|
|
|
+}
|
|
|
+
|
|
|
+function authReducer(state, { type, token }) {
|
|
|
+ if (!state) {
|
|
|
+ if (localStorage.authToken) {
|
|
|
+ type = "AUTH_LOGIN";
|
|
|
+ token = localStorage.authToken;
|
|
|
+ } else state = {};
|
|
|
+ }
|
|
|
+ if (type === "AUTH_LOGIN") {
|
|
|
+ localStorage.setItem("authToken", token);
|
|
|
+ let payload = jwtDecode(token);
|
|
|
+ if (typeof payload === "object") {
|
|
|
+ return {
|
|
|
+ ...state,
|
|
|
+ token,
|
|
|
+ payload,
|
|
|
+ };
|
|
|
+ } else return state;
|
|
|
+ }
|
|
|
+ if (type === "AUTH_LOGOUT") {
|
|
|
+ localStorage.removeItem("authToken");
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ return state;
|
|
|
+}
|
|
|
+
|
|
|
+const store = createStore(
|
|
|
+ combineReducers({
|
|
|
+ promise: promiseReducer,
|
|
|
+ auth: authReducer,
|
|
|
+ cart: cartReducer,
|
|
|
+ }),
|
|
|
+ applyMiddleware(thunk)
|
|
|
+);
|
|
|
+
|
|
|
+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>
|
|
|
+);
|
|
|
+
|
|
|
+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>;
|
|
|
+};
|
|
|
+
|
|
|
+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>
|
|
|
+);
|
|
|
+const RootCategories = ({ cats = defaultRootCats }) => (
|
|
|
+ <ul className="RootCategories">
|
|
|
+ {cats.map((cat) => (
|
|
|
+ <RootCategory cat={cat} />
|
|
|
+ ))}
|
|
|
+ </ul>
|
|
|
+);
|
|
|
+
|
|
|
+const CRootCategories = connect((state) => ({
|
|
|
+ cats: state.promise.rootCats?.payload || [],
|
|
|
+}))(RootCategories);
|
|
|
+
|
|
|
+const Aside = () => (
|
|
|
+ <aside>
|
|
|
+ <CRootCategories />
|
|
|
+ </aside>
|
|
|
+);
|
|
|
+
|
|
|
+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 GoodCard = ({ good: { _id, name, price, images } = {}, onCartAdd }) => (
|
|
|
+ <div className="GoodCard">
|
|
|
+ <h2>{name}</h2>
|
|
|
+ {images && images[0] && images[0].url && (
|
|
|
+ <img src={backURL + "/" + images[0].url} />
|
|
|
+ )}
|
|
|
+ <strong>{price}</strong>
|
|
|
+ <Button
|
|
|
+ variant="primary"
|
|
|
+ onClick={() => onCartAdd({ _id, name, price, images })}
|
|
|
+ >
|
|
|
+ +
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+);
|
|
|
+
|
|
|
+const CGoodCard = connect(null, { onCartAdd: actionCartAdd })(GoodCard);
|
|
|
+
|
|
|
+const Category = ({
|
|
|
+ cat: { _id, name, goods, subCategories } = defaultCat,
|
|
|
+}) => (
|
|
|
+ <div className="Category">
|
|
|
+ <h1>{name}</h1>
|
|
|
+ {subCategories && <SubCategories cats={subCategories} />}
|
|
|
+ {(goods || []).map((good) => (
|
|
|
+ <CGoodCard good={good} />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+);
|
|
|
+
|
|
|
+const CCategory = connect((state) => ({
|
|
|
+ cat: state.promise.catById?.payload || [],
|
|
|
+}))(Category);
|
|
|
+
|
|
|
+const CartGood = ({
|
|
|
+ cartGood: { good, count },
|
|
|
+ removeCartGood,
|
|
|
+ changeCartGood,
|
|
|
+}) => (
|
|
|
+ <div className="CartGood">
|
|
|
+ <div className="GoodImgBlock">
|
|
|
+ {good.images && good.images[0].url && (
|
|
|
+ <img src={backURL + "/" + good.images[0].url} />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <div className="GoodInfoBlock">
|
|
|
+ <h2>{good.name}</h2>
|
|
|
+ <p>Цена: {good.price * count}$</p>
|
|
|
+ <p>Всего: {count} шт.</p>
|
|
|
+ </div>
|
|
|
+ <div className="GoodEditBlock">
|
|
|
+ <Button
|
|
|
+ variant="warning"
|
|
|
+ id="DeleteBtn"
|
|
|
+ onClick={() => removeCartGood(good)}
|
|
|
+ >
|
|
|
+ Delete
|
|
|
+ </Button>
|
|
|
+ <label for="CountField">Change count</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={count}
|
|
|
+ min="1"
|
|
|
+ id="CountField"
|
|
|
+ onInput={(e) => changeCartGood(good, e.currentTarget.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+);
|
|
|
+
|
|
|
+const CCartGood = connect(null, {
|
|
|
+ removeCartGood: actionCartRemove,
|
|
|
+ changeCartGood: actionCartChange,
|
|
|
+})(CartGood);
|
|
|
+
|
|
|
+const CartPage = ({ cart, clearCart }) => (
|
|
|
+ <div className="CartMain">
|
|
|
+ <h1>Корзина</h1>
|
|
|
+
|
|
|
+ {Object.entries(cart).map(([, cartGood]) => (
|
|
|
+ <CCartGood cartGood={cartGood} />
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {Object.keys(cart).length === 0 ? (
|
|
|
+ <h2>Your cart is empty!</h2>
|
|
|
+ ) : (
|
|
|
+ <Button variant="danger" id="ClearBtn" onClick={() => clearCart()}>
|
|
|
+ Clear Cart
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+);
|
|
|
+
|
|
|
+const CCartPage = connect((state) => ({ cart: state.cart }), {
|
|
|
+ clearCart: actionCartClear,
|
|
|
+})(CartPage);
|
|
|
+
|
|
|
+const LoginForm = ({ onLogin }) => {
|
|
|
+ const [login, setLogin] = useState("");
|
|
|
+ const [password, setPassword] = useState("");
|
|
|
+ return (
|
|
|
+ <div className="LoginForm">
|
|
|
+ <input
|
|
|
+ style={{ borderColor: login.length < 1 ? "red" : "" }}
|
|
|
+ type="text"
|
|
|
+ onChange={(e) => setLogin(e.target.value)}
|
|
|
+ placeholder="Login"
|
|
|
+ />
|
|
|
+ <input
|
|
|
+ style={{ borderColor: password.length < 1 ? "red" : "" }}
|
|
|
+ type="password"
|
|
|
+ onChange={(e) => setPassword(e.target.value)}
|
|
|
+ placeholder="Password"
|
|
|
+ />
|
|
|
+ {
|
|
|
+ <button
|
|
|
+ disabled={login.length < 1 || password.length < 1 ? true : false}
|
|
|
+ onClick={() => onLogin(login, password)}
|
|
|
+ >
|
|
|
+ Login
|
|
|
+ </button>
|
|
|
+ }
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CLoginForm = connect(null, { onLogin: actionFullLogin })(LoginForm);
|
|
|
+
|
|
|
+const Main = () => (
|
|
|
+ <main>
|
|
|
+ <Aside />
|
|
|
+ <Content>
|
|
|
+ <CCategory />
|
|
|
+ </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>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+function App() {
|
|
|
+ return (
|
|
|
+ <Provider store={store}>
|
|
|
+ <div className="App">
|
|
|
+ <CLoginForm />
|
|
|
+ <RGBInput />
|
|
|
+ <Header />
|
|
|
+ <Main />
|
|
|
+ <CCartPage />
|
|
|
+ <Footer />
|
|
|
+ </div>
|
|
|
+ </Provider>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default App;
|