|
@@ -0,0 +1,533 @@
|
|
|
+import "./App.scss";
|
|
|
+import {createStore, combineReducers, applyMiddleware} from "redux";
|
|
|
+import {Provider, connect} from "react-redux";
|
|
|
+import thunk from "redux-thunk";
|
|
|
+import React, {useState, useEffect} from "react";
|
|
|
+import {Router, Route, Link, Redirect, Switch} from 'react-router-dom';
|
|
|
+import createHistory from "history/createBrowserHistory";
|
|
|
+
|
|
|
+const promiseReducer = function(state={}, {type, name, status, payload, error}) {
|
|
|
+ if (type == 'PROMISE'){
|
|
|
+ return {
|
|
|
+ ...state,
|
|
|
+ [name]:{status, payload, error}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return state;
|
|
|
+};
|
|
|
+
|
|
|
+const actionPending = name => ({type: "PROMISE", name, status: 'PENDING'});
|
|
|
+const actionFulfilled = (name,payload) => ({type: "PROMISE", name, status: 'FULFILLED', payload});
|
|
|
+const actionRejected = (name,error) => ({type: "PROMISE", name, status: 'REJECTED', error});
|
|
|
+const actionPromise = function(name, promise) {
|
|
|
+ return async dispatch => {
|
|
|
+ dispatch(actionPending(name));
|
|
|
+ try {
|
|
|
+ let payload = await promise
|
|
|
+ dispatch(actionFulfilled(name, payload))
|
|
|
+ return payload
|
|
|
+ }
|
|
|
+ catch(error){
|
|
|
+ dispatch(actionRejected(name, error))
|
|
|
+ };
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+let jwtDecode = function(token) {
|
|
|
+ let payloadInBase64;
|
|
|
+ let payloadInJson;
|
|
|
+ let payload;
|
|
|
+
|
|
|
+ try {
|
|
|
+ payloadInBase64 = token.split(".")[1];
|
|
|
+ payloadInJson = atob(payloadInBase64);
|
|
|
+ payload = JSON.parse(payloadInJson);
|
|
|
+
|
|
|
+ return payload;
|
|
|
+ }
|
|
|
+ catch(err) {
|
|
|
+
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const authReducer = function(state, {type, token}) {
|
|
|
+ let payload;
|
|
|
+
|
|
|
+ if (state == undefined) {
|
|
|
+ if(localStorage.authToken) {
|
|
|
+ type = "AUTH_LOGIN";
|
|
|
+ token = localStorage.authToken;
|
|
|
+ } else {
|
|
|
+ type = "AUTH_LOGOUT";
|
|
|
+ };
|
|
|
+ };
|
|
|
+ if (type == "AUTH_LOGIN") {
|
|
|
+ payload = jwtDecode(token);
|
|
|
+
|
|
|
+ if(payload) {
|
|
|
+ localStorage.authToken = token;
|
|
|
+ return {
|
|
|
+ token: token,
|
|
|
+ payload: payload
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+ if (type == "AUTH_LOGOUT") {
|
|
|
+ localStorage.removeItem("authToken");
|
|
|
+
|
|
|
+ return {};
|
|
|
+ };
|
|
|
+
|
|
|
+ return state || {};
|
|
|
+};
|
|
|
+
|
|
|
+const actionAuthLogin = token => ({type: "AUTH_LOGIN", token});
|
|
|
+const actionAuthLogout = () => ({type: "AUTH_LOGOUT"});
|
|
|
+const actionFullLogin = function(login, password) {
|
|
|
+ return async dispatch => {
|
|
|
+ let token = await gql("query userLogin($login: String, $password: String) {login(login: $login, password: $password)}", {"login": login, "password": password});
|
|
|
+ dispatch(actionAuthLogin(token));
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+let actionFullRegister = function(login, password, nick) {
|
|
|
+ return async dispatch => {
|
|
|
+ dispatch(actionPromise("userRegister", gql(`mutation userRegister($login:String, $password:String, $nick:String) {
|
|
|
+ UserUpsert(user: {login:$login, password:$password, nick:$nick}) {
|
|
|
+ _id login nick
|
|
|
+ }
|
|
|
+ }`,
|
|
|
+ {
|
|
|
+ "login": login,
|
|
|
+ "password": password,
|
|
|
+ "nick": nick
|
|
|
+ })));
|
|
|
+ dispatch(actionFullLogin(login, password));
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+let cartReducer = function(state={}, {type, good, count=1}) {
|
|
|
+ if(type == "CART_ADD") {
|
|
|
+ let newState = {...state};
|
|
|
+
|
|
|
+ if(good["_id"] in state) {
|
|
|
+ newState[good._id] = {count: newState[good._id].count + count, good}
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ newState = {
|
|
|
+ ...state,
|
|
|
+ [good._id]: {count, good}
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ return newState;
|
|
|
+ };
|
|
|
+ if(type == "CART_CHANGE") {
|
|
|
+ let newState = {...state,
|
|
|
+ [good._id]: {count, good}
|
|
|
+ };
|
|
|
+
|
|
|
+ return newState;
|
|
|
+ };
|
|
|
+ if(type == "CART_DELETE") {
|
|
|
+ let newState = {...state};
|
|
|
+
|
|
|
+ delete newState[good._id];
|
|
|
+
|
|
|
+ return newState;
|
|
|
+ };
|
|
|
+ if(type == "CART_CLEAR") {
|
|
|
+ return {};
|
|
|
+ };
|
|
|
+
|
|
|
+ return state;
|
|
|
+};
|
|
|
+
|
|
|
+const actionCartAdd = (good, count=1) => ({type: 'CART_ADD', good, count: +count})
|
|
|
+const actionCartChange = (good, count=1) => ({type: 'CART_CHANGE', good, count: +count})
|
|
|
+const actionCartDelete = (good) => ({type: 'CART_DELETE', good})
|
|
|
+const actionCartClear = () => ({type: 'CART_CLEAR'})
|
|
|
+
|
|
|
+const getGQL = function(url) {
|
|
|
+ return async function(query, variables) {
|
|
|
+ const res = await fetch(url, {
|
|
|
+ method: "POST",
|
|
|
+ headers: {
|
|
|
+ "Content-Type": "application/json",
|
|
|
+ ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } : {})
|
|
|
+ },
|
|
|
+ body: JSON.stringify({ query, variables })
|
|
|
+ });
|
|
|
+ const data = await res.json();
|
|
|
+ if (data.data) {
|
|
|
+ return Object.values(data.data)[0];
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new Error(JSON.stringify(data.errors));
|
|
|
+ };
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const backendURL = 'http://shop-roles.asmer.fs.a-level.com.ua';
|
|
|
+
|
|
|
+const gql = getGQL(backendURL + '/graphql');
|
|
|
+
|
|
|
+let actionRootCats = function() {
|
|
|
+ return actionPromise("rootCats", gql(`query {
|
|
|
+ CategoryFind(query: "[{\\"parent\\":null}]"){
|
|
|
+ _id name
|
|
|
+ }
|
|
|
+ }`));
|
|
|
+};
|
|
|
+
|
|
|
+let actionCatById = function(_id) {
|
|
|
+ return actionPromise("catById", gql(`query catById($q: String){
|
|
|
+ CategoryFindOne(query: $q){
|
|
|
+ _id name goods {
|
|
|
+ _id name price images {
|
|
|
+ url
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }`,
|
|
|
+ {q: JSON.stringify([{_id}])}
|
|
|
+ ));
|
|
|
+};
|
|
|
+
|
|
|
+let actionGoodById = function(_id) {
|
|
|
+ return actionPromise("goodById", gql(`query findGood($goodQuery: String) {
|
|
|
+ GoodFindOne(query:$goodQuery) {
|
|
|
+ _id name price images {
|
|
|
+ url
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }`,
|
|
|
+ {goodQuery: JSON.stringify([{"_id": _id}])}
|
|
|
+ ));
|
|
|
+};
|
|
|
+
|
|
|
+let actionOrders = async function() {
|
|
|
+ let order = await gql(`mutation makeOrder($order:OrderInput){
|
|
|
+ OrderUpsert(order: $order){
|
|
|
+ _id
|
|
|
+ }
|
|
|
+ }`, {
|
|
|
+ "order": {
|
|
|
+ orderGoods: Object.entries(store.getState().cart).map(([_id, count]) =>({"count": count.count, "good": {_id}}))
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ store.dispatch(actionCartClear());
|
|
|
+}
|
|
|
+
|
|
|
+let actionOrdersFind = function() {
|
|
|
+ return actionPromise("ordersFind", gql(`query ordersFind($query:String) {
|
|
|
+ OrderFind(query: $query) {
|
|
|
+ createdAt orderGoods {
|
|
|
+ count good {
|
|
|
+ name price images {
|
|
|
+ url
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }`,
|
|
|
+ {
|
|
|
+ "query": JSON.stringify([{}])
|
|
|
+ }));
|
|
|
+}
|
|
|
+
|
|
|
+const store = createStore(combineReducers({promise: promiseReducer,
|
|
|
+ auth: authReducer,
|
|
|
+ cart: cartReducer}), applyMiddleware(thunk));
|
|
|
+
|
|
|
+store.subscribe(() => console.log(store.getState()));
|
|
|
+store.dispatch(actionRootCats());
|
|
|
+
|
|
|
+
|
|
|
+let Nav = function({auth}) {
|
|
|
+ return (
|
|
|
+ <ul className="nav">
|
|
|
+ <li>
|
|
|
+ <Link to="/login">Логин</Link>
|
|
|
+ </li>
|
|
|
+ <li>
|
|
|
+ <Link to="/registration">Регистрация</Link>
|
|
|
+ </li>
|
|
|
+ <li>
|
|
|
+ <Link to="/cart">Корзина</Link>
|
|
|
+ </li>
|
|
|
+ <li>
|
|
|
+ <Link to="/dashboard">История покупок</Link>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CNav = connect(state => ({auth: state.auth}))(Nav);
|
|
|
+
|
|
|
+let Header = function() {
|
|
|
+ return (
|
|
|
+ <header className="header">
|
|
|
+ <div className="header__logo">Типо логотип</div>
|
|
|
+ <CNav />
|
|
|
+ </header>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+let RootCategories = function({rootCats}) {
|
|
|
+ return (
|
|
|
+ <aside className="rootCats">
|
|
|
+ <ul className="rootCats__list">
|
|
|
+ {rootCats.map(rootCat => <li><Link to={`/category/${rootCat._id}`}>{rootCat.name}</Link></li>)}
|
|
|
+ </ul>
|
|
|
+ </aside>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CRootCategories = connect(state => ({rootCats: state.promise?.rootCats?.payload || []}))(RootCategories);
|
|
|
+
|
|
|
+let MainPage = function() {
|
|
|
+ return (
|
|
|
+ <h1>Главная страничка</h1>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+let GoodCard = function({good}) {
|
|
|
+ return (
|
|
|
+ <li className="goods__item">
|
|
|
+ <img src={`${backendURL}/${good.images[0].url}`} />
|
|
|
+ <b>{good.name}</b><br />
|
|
|
+ <span>Цена: {good.price}</span><br />
|
|
|
+ <Link to={`/good/${good._id}`}>Перейти на страничку товара</Link>
|
|
|
+ </li>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+let Goods = function({goods}) {
|
|
|
+ return (
|
|
|
+ <section className="goods">
|
|
|
+ <ul className="goods__list">
|
|
|
+ {goods.map(good => <GoodCard good={good} />)}
|
|
|
+ </ul>
|
|
|
+ </section>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CGoods = connect(state => ({goods: state.promise?.catById?.payload?.goods || []}))(Goods);
|
|
|
+
|
|
|
+let Categories = function({match: {params: {_id}}, catById}) {
|
|
|
+ catById(_id);
|
|
|
+ return (
|
|
|
+ <CGoods />
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CCategories = connect(null, {catById: actionCatById})(Categories);
|
|
|
+
|
|
|
+let GoodPageCard = function({good, cartAdd}) {
|
|
|
+ return (
|
|
|
+ <section className="good">
|
|
|
+ <img src={`${backendURL}/${good?.images[0]?.url}`} />
|
|
|
+ <div>
|
|
|
+ <b>{good?.name}</b><br />
|
|
|
+ <span>Цена: {good?.price}</span><br />
|
|
|
+ <button onClick={() => cartAdd(good)}>Добавить в корзину</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CGoodPageCard = connect(state => ({good: state.promise?.goodById?.payload}),
|
|
|
+ {cartAdd: actionCartAdd})(GoodPageCard)
|
|
|
+
|
|
|
+let GoodPage = function({match: {params: {_id}}, goodById}) {
|
|
|
+ goodById(_id);
|
|
|
+ return (
|
|
|
+ <CGoodPageCard />
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CGoodPage = connect(null, {goodById: actionGoodById})(GoodPage);
|
|
|
+
|
|
|
+let Login = function({fullLogin}) {
|
|
|
+ const [login, setLogin] = useState("");
|
|
|
+ const [password, setPassword] = useState("");
|
|
|
+
|
|
|
+ return (
|
|
|
+ <section className="login">
|
|
|
+ <h2>Логин</h2>
|
|
|
+ <input type="text"
|
|
|
+ placeholder="Введите ваш логин"
|
|
|
+ onChange={evt => setLogin(evt.target.value)} /><br />
|
|
|
+ <input type="password"
|
|
|
+ placeholder="Введите ваш пароль"
|
|
|
+ onChange={evt => setPassword(evt.target.value)} /><br />
|
|
|
+ <button onClick={() => fullLogin(login, password)}>Залогинится</button>
|
|
|
+ </section>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CLogin = connect(null, {fullLogin: actionFullLogin})(Login);
|
|
|
+
|
|
|
+let Registration = function({fullRegister}) {
|
|
|
+ const [login, setLogin] = useState("");
|
|
|
+ const [nick, setNick] = useState("");
|
|
|
+ const [password, setPassword] = useState("");
|
|
|
+
|
|
|
+ return (
|
|
|
+ <section className="registration">
|
|
|
+ <h2>Регистрация</h2>
|
|
|
+ <input type="text"
|
|
|
+ placeholder="Введите ваш логин"
|
|
|
+ onChange={evt => setLogin(evt.target.value)} /><br/>
|
|
|
+ <input type="text"
|
|
|
+ placeholder="Введите ваш никнейм"
|
|
|
+ onChange={evt => setNick(evt.target.value)} /><br/>
|
|
|
+ <input type="password"
|
|
|
+ placeholder="Введите ваш пароль"
|
|
|
+ onChange={evt => setPassword(evt.target.value)} /><br/>
|
|
|
+ <button onClick={() => fullRegister(login, password, nick)}>Зарегистрироваться</button>
|
|
|
+ </section>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CRegistration = connect(null, {fullRegister: actionFullRegister})(Registration)
|
|
|
+
|
|
|
+let CardGood = function({good: {count, good}, cartChange, cartDelete}) {
|
|
|
+ const [value, setValue] = useState(count);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <tr>
|
|
|
+ <td>
|
|
|
+ <img className="cart__img"
|
|
|
+ src={`${backendURL}/${good?.images[0]?.url}`} />
|
|
|
+ </td>
|
|
|
+ <td>{good?.name}</td>
|
|
|
+ <td>Цена: {good?.price}</td>
|
|
|
+ <td>Количество:
|
|
|
+ <input type="number"
|
|
|
+ value={value}
|
|
|
+ onChange={evt => {
|
|
|
+ setValue(evt.target.value);
|
|
|
+ cartChange(good, evt.target.value);
|
|
|
+ }}/>
|
|
|
+ </td>
|
|
|
+ <td>
|
|
|
+ <button onClick={() => cartDelete(good)}>Удоли</button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CCardGood = connect(null, {cartChange: actionCartChange,
|
|
|
+ cartDelete: actionCartDelete})(CardGood);
|
|
|
+
|
|
|
+let Cart = function({cart, makeOrder}) {
|
|
|
+ return (
|
|
|
+ <section className="cart">
|
|
|
+ <h2>Корзина</h2>
|
|
|
+ <table>
|
|
|
+ {cart.map(good => <CCardGood good={good} />)}
|
|
|
+ </table>
|
|
|
+ <button onClick={() => makeOrder()}>Сделать заказ</button>
|
|
|
+ </section>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CCart = connect(state => ({cart: Object.values(state.cart) || []}),
|
|
|
+ {makeOrder: actionOrders})(Cart);
|
|
|
+
|
|
|
+let OrderGood = function({good: {count, good}}) {
|
|
|
+ return (
|
|
|
+ <li>
|
|
|
+ <ul className="dashboard__good-list">
|
|
|
+ <li>
|
|
|
+ <img className="dashboard__img"
|
|
|
+ src={`${backendURL}/${good?.images[0]?.url}`} />
|
|
|
+ </li>
|
|
|
+ <li>{good?.name}</li>
|
|
|
+ <li>Цена: {good?.price}</li>
|
|
|
+ <li>Количество: {count}</li>
|
|
|
+ </ul>
|
|
|
+ </li>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+let Order = function({order: {createdAt, orderGoods}}) {
|
|
|
+ return (
|
|
|
+ <tr>
|
|
|
+ <td className="dashboard__date">{(new Date(+createdAt)).toLocaleString()}</td>
|
|
|
+ <td>
|
|
|
+ <ul className="dashboard__list">
|
|
|
+ {orderGoods.map(orderGood => <OrderGood good={orderGood} />)}
|
|
|
+ </ul>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+let Dashboard = function({orders}) {
|
|
|
+ return (
|
|
|
+ <table>
|
|
|
+ {orders.map(order => <Order order={order} />)}
|
|
|
+ </table>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CDashboardTable = connect(state => ({orders: state.promise?.ordersFind?.payload || []}))(Dashboard)
|
|
|
+
|
|
|
+let PreDashboard = function({orderFind}) {
|
|
|
+ orderFind();
|
|
|
+
|
|
|
+ return (
|
|
|
+ <section className="dashboard">
|
|
|
+ <h2>История заказов</h2>
|
|
|
+ <CDashboardTable />
|
|
|
+ </section>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const CDashboard = connect(null, {orderFind: actionOrdersFind})(PreDashboard);
|
|
|
+
|
|
|
+let Content = function() {
|
|
|
+ return (
|
|
|
+ <Switch>
|
|
|
+ <Route path="/" exact component={MainPage} />
|
|
|
+ <Route path="/category/:_id" exact component={CCategories} />
|
|
|
+ <Route path="/good/:_id" exact component={CGoodPage} />
|
|
|
+ <Route path="/login" exact component={CLogin} />
|
|
|
+ <Route path="/registration" exact component={CRegistration} />
|
|
|
+ <Route path="/cart" exact component={CCart} />
|
|
|
+ <Route path="/dashboard" exact component={CDashboard} />
|
|
|
+ </Switch>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+let Main = function() {
|
|
|
+ return (
|
|
|
+ <main className="main">
|
|
|
+ <CRootCategories />
|
|
|
+ <Content />
|
|
|
+ </main>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const history = createHistory()
|
|
|
+
|
|
|
+function App() {
|
|
|
+ return (
|
|
|
+ <Router history={history}>
|
|
|
+ <Provider store={store}>
|
|
|
+ <div className="App">
|
|
|
+ <Header />
|
|
|
+ <Main />
|
|
|
+ </div>
|
|
|
+ </Provider>
|
|
|
+ </Router>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default App;
|