|
@@ -1,25 +1,475 @@
|
|
-import logo from './logo.svg';
|
|
|
|
-import './App.css';
|
|
|
|
-
|
|
|
|
-function App() {
|
|
|
|
- return (
|
|
|
|
- <div className="App">
|
|
|
|
- <header className="App-header">
|
|
|
|
- <img src={logo} className="App-logo" alt="logo" />
|
|
|
|
- <p>
|
|
|
|
- Edit <code>src/App.js</code> and save to reload.
|
|
|
|
- </p>
|
|
|
|
- <a
|
|
|
|
- className="App-link"
|
|
|
|
- href="https://reactjs.org"
|
|
|
|
- target="_blank"
|
|
|
|
- rel="noopener noreferrer"
|
|
|
|
- >
|
|
|
|
- Learn React
|
|
|
|
- </a>
|
|
|
|
- </header>
|
|
|
|
|
|
+import React, { useState } from 'react';
|
|
|
|
+import defaultLogo from './logo.svg';
|
|
|
|
+import './App.scss';
|
|
|
|
+import thunk from 'redux-thunk';
|
|
|
|
+import { Provider, connect } from 'react-redux';
|
|
|
|
+import { createStore, combineReducers, applyMiddleware} from 'redux';
|
|
|
|
+
|
|
|
|
+const getGQL = url =>
|
|
|
|
+(query, variables) => fetch(url, {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ headers: {
|
|
|
|
+ "Content-Type": "application/json",
|
|
|
|
+ ...(localStorage.authToken ? {"Authorization": "Bearer " + localStorage.authToken} : {})
|
|
|
|
+ },
|
|
|
|
+ body: JSON.stringify({query, variables})
|
|
|
|
+}).then(res => res.json())
|
|
|
|
+ .then(data => {
|
|
|
|
+ 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');
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}), applyMiddleware(thunk))
|
|
|
|
+store.subscribe(() => console.log(store.getState()));
|
|
|
|
+
|
|
|
|
+function promiseReducer(state={}, {type,name,status,payload,error}){
|
|
|
|
+ /* delay1000:{status, payload,error}
|
|
|
|
+ delay2000: {status, payload,error}*/
|
|
|
|
+ /* if(!state){
|
|
|
|
+ delay1000:{status,payload,error}
|
|
|
|
+ delay2000:{status,payload,error}
|
|
|
|
+ } */
|
|
|
|
+ if(type === 'PROMISE'){
|
|
|
|
+ return{
|
|
|
|
+ ...state,
|
|
|
|
+ [name]:{status,payload,error}
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return state
|
|
|
|
+}
|
|
|
|
+const actionPending = name => ({name,type:"PROMISE",status:'PENDING'})
|
|
|
|
+const actionFulfiled = (name,payload) =>({name,type:"PROMISE",status:'FULFILLED',payload})
|
|
|
|
+const actionrRejected = (name,error) => ({name,type:"PROMISE",status:'REJECTED',error})
|
|
|
|
+
|
|
|
|
+const actionPromise = (name,promise) =>
|
|
|
|
+ async dispatch => {
|
|
|
|
+ dispatch(actionPending(name))
|
|
|
|
+ try{
|
|
|
|
+ let payload = await promise
|
|
|
|
+ dispatch(actionFulfiled(name,payload))
|
|
|
|
+ return payload
|
|
|
|
+ }
|
|
|
|
+ catch(e){
|
|
|
|
+ dispatch(actionrRejected(name,e))
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+/* const jwtDecode = (token) =>{
|
|
|
|
+ try{
|
|
|
|
+ let payload = JSON.parse(atob(token.split('.')[1]));
|
|
|
|
+ return payload;
|
|
|
|
+ } catch(e){
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+} */
|
|
|
|
+
|
|
|
|
+function jwtDecode(token){
|
|
|
|
+ try{
|
|
|
|
+ let payload = JSON.parse(atob(token.split('.')[1]));
|
|
|
|
+ return payload;
|
|
|
|
+} catch(e){
|
|
|
|
+
|
|
|
|
+}
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function authReducer(state,{type, token}){
|
|
|
|
+ if(state === undefined){
|
|
|
|
+ if(localStorage.authToken){
|
|
|
|
+ type = 'AUTH_LOGIN';
|
|
|
|
+ token = localStorage.authToken
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if(type === 'AUTH_LOGIN'){
|
|
|
|
+ let payload = jwtDecode(token);
|
|
|
|
+ if(payload){
|
|
|
|
+ localStorage.authToken = token;
|
|
|
|
+ return {token,payload}
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if(type === 'AUTH_LOGOUT'){
|
|
|
|
+ localStorage.authToken = '';
|
|
|
|
+ return {};
|
|
|
|
+ }
|
|
|
|
+ return state || {};
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function cartReducer(state={},{type, good, count=1}){
|
|
|
|
+
|
|
|
|
+/* _id1: {count:1, good: {_id1,name,price,images}}
|
|
|
|
+_id2: {count:1, good: {_id2,name,price,images}}
|
|
|
|
+*/
|
|
|
|
+
|
|
|
|
+if (type === 'CART_ADD'){
|
|
|
|
+ /* if(good._id in state){
|
|
|
|
+ return{
|
|
|
|
+ ...state,
|
|
|
|
+ [good._id]: {count:state[good._id].count+count,good}
|
|
|
|
+ }
|
|
|
|
+ }else{ */
|
|
|
|
+ return{
|
|
|
|
+ ...state,
|
|
|
|
+ [good._id] : {count: count + (good._id in state ? state[good._id].count : 0),good}
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+if (type === 'CART_CHANGE'){
|
|
|
|
+ /* let _id1 = good._id;
|
|
|
|
+ console.log(_id1); */
|
|
|
|
+ return{
|
|
|
|
+ ...state,
|
|
|
|
+ [good._id]: {count:count,good:good}
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+if (type === 'CART_DELETE'){
|
|
|
|
+ /* const clone = {...state}
|
|
|
|
+ delete clone[good._id]
|
|
|
|
+ state = {...clone} */
|
|
|
|
+ let {[good._id]: remove, ...newState} = state;
|
|
|
|
+ return newState;
|
|
|
|
+}
|
|
|
|
+if (type === 'CART_CLEAN'){
|
|
|
|
+ return {}
|
|
|
|
+}
|
|
|
|
+return state
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const actionCartAdd = (good, count=1) => ({type:'CART_ADD', good, count})
|
|
|
|
+const actionCartChange = (good, count=1) => ({type:'CART_CHANGE',good,count:+count})
|
|
|
|
+const actionCartDelete = (good) => ({type: 'CART_DELETE',good})
|
|
|
|
+const actionCartClean = () => ({type:'CART_CLEAN'})
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+const actionAuthLogin = (tokennn) => ({type:'AUTH_LOGIN',token:tokennn})
|
|
|
|
+const actionAuthLogout= () => ({type: 'AUTH_LOGOUT'})
|
|
|
|
+
|
|
|
|
+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){
|
|
|
|
+ _id name goods {
|
|
|
|
+ _id name price images {
|
|
|
|
+ url
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }`, {q: JSON.stringify([{_id}])}))
|
|
|
|
+const actionGoodById = (_id) =>
|
|
|
|
+ actionPromise('goodById', gql(`query goodById ($goodId: String){
|
|
|
|
+ GoodFindOne(query:$goodId){
|
|
|
|
+ _id name price images{
|
|
|
|
+ url
|
|
|
|
+ }
|
|
|
|
+ description
|
|
|
|
+ }
|
|
|
|
+ }`,{goodId: JSON.stringify([{_id}])}))
|
|
|
|
+
|
|
|
|
+const actionFullRegister = (Login,password) =>
|
|
|
|
+ actionPromise('fullRegister', gql(`mutation register($Login:String,$password:String){
|
|
|
|
+ UserUpsert(user:{login:$Login,password:$password}){
|
|
|
|
+ _id login
|
|
|
|
+ }
|
|
|
|
+ }`,{Login: Login, password: password}))
|
|
|
|
+
|
|
|
|
+const actionFullLogin = (Login,password) => async (dispatch) => {
|
|
|
|
+ const tokennn = await dispatch(
|
|
|
|
+ actionPromise('fullLogin', gql(`query login($Login:String,$password:String){
|
|
|
|
+ login(login:$Login,password:$password)
|
|
|
|
+ }`,{Login:Login,password:password}))
|
|
|
|
+ );
|
|
|
|
+ await dispatch(actionAuthLogin(tokennn));
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const actionAllOrders = () =>
|
|
|
|
+ actionPromise('allOrders',gql(`query orders{
|
|
|
|
+ OrderFind(query:"[{}]"){
|
|
|
|
+ _id total
|
|
|
|
+ orderGoods{
|
|
|
|
+ count total good{name,price,
|
|
|
|
+ images{
|
|
|
|
+ url
|
|
|
|
+ }}
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }`))
|
|
|
|
+const actionCreateOrder = () => async (dispatch) =>{
|
|
|
|
+ let orderGoods = [];
|
|
|
|
+ Object.entries(store.getState().cart).map(([_id,{count}])=>orderGoods.push({"count":count,"good":{_id:_id}}))
|
|
|
|
+ actionPromise('createOrder',gql(`mutation newOrder($order:OrderInput){
|
|
|
|
+ OrderUpsert(order:$order){
|
|
|
|
+ _id total
|
|
|
|
+ }
|
|
|
|
+ }`,{order:{orderGoods}}));
|
|
|
|
+ await store.dispatch(actionCartClean());
|
|
|
|
+ }
|
|
|
|
+const actionSearchGood = (good) =>
|
|
|
|
+ actionPromise('searchGood', gql(`query gf($query: String){
|
|
|
|
+ GoodFind(query: $query){
|
|
|
|
+ _id, name, description, price, images{
|
|
|
|
+ _id, url
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }`, {query: JSON.stringify([
|
|
|
|
+ {
|
|
|
|
+ $or: [{name: `/${good}/`}, {description: `/${good}/`}] //регулярки пишутся в строках
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ sort: [{name: 1}]} //сортируем по title алфавитно
|
|
|
|
+ ])
|
|
|
|
+ }))
|
|
|
|
+store.dispatch(actionRootCats());
|
|
|
|
+store.dispatch(actionCatById("5dc458985df9d670df48cc47"))
|
|
|
|
+store.dispatch(actionAllOrders());
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+let defaultCategories = [{
|
|
|
|
+ "name": " Smartphones",
|
|
|
|
+ "_id": "5dc458985df9d670df48cc47"
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": "Холодильники",
|
|
|
|
+ "_id": "5dc4b2553f23b553bf3540fc"
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": "Стиральные машины",
|
|
|
|
+ "_id": "5dc4b2553f23b553bf3540fd"
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": "Стирально-сушильные машины",
|
|
|
|
+ "_id": "5dc4b2553f23b553bf3540fe"
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": "Стиральные машины",
|
|
|
|
+ "_id": "5dc4b2553f23b553bf3540ff"
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": "Духовые шкафы",
|
|
|
|
+ "_id": "5dc4b2553f23b553bf354100"
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": "Крупная бытовая техника",
|
|
|
|
+ "_id": "5dc4b2553f23b553bf354101"
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": " Макароны",
|
|
|
|
+ "_id": "5dcac1b56d09c45440d14cf8"
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": "Drinks",
|
|
|
|
+ "_id": "5dcac6cf6d09c45440d14cfd"
|
|
|
|
+}]
|
|
|
|
+
|
|
|
|
+let defaultGoods = [{
|
|
|
|
+ "name": "tv",
|
|
|
|
+ "_id": "61afa8e0c750c12ba6ba444d",
|
|
|
|
+ "price": 443,
|
|
|
|
+ "images": [
|
|
|
|
+ {
|
|
|
|
+ "url": "images/b0e4301f809296e1cb52c81936e9b41c"
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+},
|
|
|
|
+{
|
|
|
|
+ "name": "От Прохора",
|
|
|
|
+ "_id": "61782338ef4e1b3e3b677079",
|
|
|
|
+ "price": 10000,
|
|
|
|
+ "images": [
|
|
|
|
+ {
|
|
|
|
+ "url": "images/d650672a6df67bfb4fb69bacd098981b"
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ "url": "images/1a16d34cd470d7b6d7bd4d5acad30d81"
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+}]
|
|
|
|
+
|
|
|
|
+let defaultOrders = [{
|
|
|
|
+ "_id": "61c1ed7ac750c12ba6ba4bf5",
|
|
|
|
+ "total": 13900,
|
|
|
|
+ "orderGoods": [
|
|
|
|
+ {
|
|
|
|
+ "count": 2,
|
|
|
|
+ "total": 10000,
|
|
|
|
+ "good": {
|
|
|
|
+ "name": "Apple iPhone X 64GB Space Gray",
|
|
|
|
+ "price": 5000,
|
|
|
|
+ "images": [
|
|
|
|
+ {
|
|
|
|
+ "url": "images/00505f5f08ac113874318dee67975aa9"
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ "count": 3,
|
|
|
|
+ "total": 3900,
|
|
|
|
+ "good": {
|
|
|
|
+ "name": "Apple iPhone XS Max 256GB Gold",
|
|
|
|
+ "price": 1300,
|
|
|
|
+ "images": [
|
|
|
|
+ {
|
|
|
|
+ "url": "images/63c4a052377862494e33746b375903f6"
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+}]
|
|
|
|
+const CategoryItem = ({category:{_id,name}= {}}) =>
|
|
|
|
+<li className='CategoryItem'>
|
|
|
|
+ <a target="_blank" href={_id}>{name}</a>
|
|
|
|
+</li>
|
|
|
|
+const ProductItem = ({good:{_id,name,price,description,images} = {}}) =>
|
|
|
|
+{ let source = backendURL+"/"+images[0].url;
|
|
|
|
+ let good = {_id,name,price,images}
|
|
|
|
+ return (<div className='ProductItem'>
|
|
|
|
+ <h2 className='ProductCartName'>{name}</h2>
|
|
|
|
+ <img src={source}/>
|
|
|
|
+ <strong>{price}</strong>
|
|
|
|
+ <p>{description}</p>
|
|
|
|
+ <button onClick={() => {store.dispatch(actionCartAdd(good))}}>Купить</button>
|
|
|
|
+
|
|
|
|
+</div>)
|
|
|
|
+}
|
|
|
|
+const OrderGoodItem = ({orderedGoods:{count,total,good} = {}}) =>
|
|
|
|
+<div>
|
|
|
|
+<h3>{good.name}</h3>
|
|
|
|
+<strong>price :{good.price}</strong>
|
|
|
|
+<img src={`${backendURL}/${good.images[0].url}`}/>
|
|
|
|
+<span>total count: {count }</span>
|
|
|
|
+<strong> total price : {total}</strong>
|
|
|
|
+</div>
|
|
|
|
+const OrderItem = ({order:{_id,total,orderGoods} = {}}) =>
|
|
|
|
+{ /* console.log(orderGoods[0].good.name); */
|
|
|
|
+ /* let images = orderGoods.good.images;
|
|
|
|
+ let source = backendURL+"/"+images[0].url */
|
|
|
|
+ return(<div className='OrderItem'>
|
|
|
|
+ <h2>{_id}</h2>
|
|
|
|
+ <strong>{total}</strong>
|
|
|
|
+ {orderGoods.map(orderedGoods => <OrderGoodItem orderedGoods={orderedGoods}/>)}
|
|
|
|
+ </div>)
|
|
|
|
+}
|
|
|
|
+const CartGood = ({good:{count,good} = {},onChange,onDelete}) =>
|
|
|
|
+{ console.log(count);
|
|
|
|
+ return(<div className='cartGood'>
|
|
|
|
+ <h2>{good.name}</h2>
|
|
|
|
+ <img src={`${backendURL}/${good.images[0].url}`}/>
|
|
|
|
+ <input type="number" value={count} onChange={evt => onChange(good,evt.target.value)}/>
|
|
|
|
+ <button onClick={() => onDelete(good)}>Delete</button>
|
|
|
|
+ </div>)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const CCartGood = connect(null,{onChange: actionCartChange, onDelete: actionCartDelete})(CartGood)
|
|
|
|
+
|
|
|
|
+const CartGoods = ({goodds}) => {
|
|
|
|
+ console.log(goodds);
|
|
|
|
+ return(
|
|
|
|
+ <div>
|
|
|
|
+ <h1>КОРЗИНА</h1>
|
|
|
|
+{goodds.map(good => <CCartGood good={good}/>)}
|
|
|
|
+ </div>
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const LoginForm = ({Login}) =>{
|
|
|
|
+ const [login,setLogin] = useState('');
|
|
|
|
+ const [pswd,setPswd] = useState('');
|
|
|
|
+
|
|
|
|
+ return(
|
|
|
|
+ <div className='LoginForm'>
|
|
|
|
+ <label>Login <input placeholder='login' type="text" onChange={event => setLogin(event.target.value)}/></label>
|
|
|
|
+ <label>Password <input placeholder='password'type="password" onChange={event => setPswd(event.target.value)}/></label>
|
|
|
|
+ <button onClick={() => Login(login,pswd)}>login</button>
|
|
</div>
|
|
</div>
|
|
- );
|
|
|
|
|
|
+ )
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+const CLoginForm = connect(state=>({auth: state.auth?.payload || []}),{Login: actionFullLogin})(LoginForm)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+const RootCategories = ({categories = defaultCategories}) =>
|
|
|
|
+<ul className='RootCategories'>
|
|
|
|
+ {categories.map(category => <CategoryItem category={category}/>)}
|
|
|
|
+</ul>
|
|
|
|
+const ProductCart = ({goods = defaultGoods}) =>
|
|
|
|
+<div>
|
|
|
|
+{goods.map(good => <ProductItem good={good}/>)}
|
|
|
|
+</div>
|
|
|
|
+const AllOrders = ({orders = defaultOrders}) =>
|
|
|
|
+<div>
|
|
|
|
+ {orders.map(order => <OrderItem order={order}/>)}
|
|
|
|
+</div>
|
|
|
|
+const CCartGoods = connect(state => ({goodds: Object.values(state.cart)}))(CartGoods)
|
|
|
|
+const CRootCategories = connect(state => ({categories: state.promise.rootCats.payload || []}),)(RootCategories)
|
|
|
|
+const CLogout = connect(state => ({children: `logout(${state.auth.payload && state.auth.payload.sub.login || 'anon'})`}),
|
|
|
|
+ {onClick: actionAuthLogout})("button")
|
|
|
|
+const CategoriesProducts = connect(state =>({goods:state.promise.catById?.payload?.goods}),)(ProductCart)
|
|
|
|
+const Orders = connect(state =>({orders: state.promise.allOrders?.payload || []}))(AllOrders)
|
|
|
|
+
|
|
|
|
+const Logo = ({logo=defaultLogo}) =>
|
|
|
|
+<img className="Logo" src={logo} alt='logo'/>
|
|
|
|
+
|
|
|
|
+const Header = ({children}) =>
|
|
|
|
+<header>
|
|
|
|
+ <Logo />
|
|
|
|
+ <CLoginForm/>
|
|
|
|
+ {children}
|
|
|
|
+ <CLogout/>
|
|
|
|
+</header>
|
|
|
|
|
|
-export default App;
|
|
|
|
|
|
+
|
|
|
|
+const Aside = ({}) =>
|
|
|
|
+<aside>
|
|
|
|
+ <CRootCategories/>
|
|
|
|
+</aside>
|
|
|
|
+const Content = ({children}) =>
|
|
|
|
+<section className='Content'>
|
|
|
|
+
|
|
|
|
+ {children}
|
|
|
|
+</section>
|
|
|
|
+
|
|
|
|
+const Main = ({children}) =>
|
|
|
|
+<main>
|
|
|
|
+ <Aside/>
|
|
|
|
+ <Content>
|
|
|
|
+ {/* <Logo/> */}
|
|
|
|
+ <CategoriesProducts/>
|
|
|
|
+ <CCartGoods/>
|
|
|
|
+ <Orders/>
|
|
|
|
+ {children}
|
|
|
|
+ </Content>
|
|
|
|
+ {children}
|
|
|
|
+</main>
|
|
|
|
+
|
|
|
|
+const Footer = ({children}) =>
|
|
|
|
+<footer>
|
|
|
|
+ <Logo/>
|
|
|
|
+ {children}
|
|
|
|
+</footer>
|
|
|
|
+function App () {
|
|
|
|
+ return(
|
|
|
|
+ <Provider store={store}>
|
|
|
|
+ <div className='App'>
|
|
|
|
+ <Header />
|
|
|
|
+ <Main />
|
|
|
|
+ <Footer />
|
|
|
|
+ </div>
|
|
|
|
+ </Provider>
|
|
|
|
+ );
|
|
|
|
+}
|
|
|
|
+export default App;
|