Переглянути джерело

added Protect Roter and local StorageReducer DONE

makstravm 2 роки тому
батько
коміт
61610610a1

+ 1 - 0
package.json

@@ -8,6 +8,7 @@
         "@testing-library/user-event": "^13.5.0",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
+        "react-dropzone": "^11.5.1",
         "react-redux": "^7.2.6",
         "react-router-dom": "5.3.0",
         "react-scripts": "5.0.0",

+ 148 - 2
src/App.js

@@ -1,15 +1,19 @@
 import './App.scss';
-import { Router, Route } from 'react-router-dom';
+import { Router, Route, Redirect } from 'react-router-dom';
 import createHistory from "history/createBrowserHistory";
 import { Provider } from 'react-redux';
 import { Header } from './components/header/Header';
 import { Main } from './components/main/Main';
 import { Footer } from './components/Footer';
-import { store } from './redux-Store/redux-Store'
+import { backURL, store } from './redux-Store/redux-Store'
 import { actionPromise } from './redux-Store/promiseReducer'
 import { gql } from './redux-Store/redux-Store'
 import { CLoginForm } from './components/authorization/LoginForm';
 import { CRegisterForm } from './components/authorization/RegisterForm';
+import { connect } from 'react-redux';
+import { useCallback } from 'react';
+import { useDropzone } from 'react-dropzone';
+import { useState } from 'react';
 
 const actionRootCats = () =>
     actionPromise('rootCats', gql(`query {
@@ -18,17 +22,159 @@ const actionRootCats = () =>
             }
         }`))
 
+export function routeReducer(state = {}, { type, match }) {
+    if (type === 'ROUTE') {
+        return match
+    }
+    return state
+}
+
+const RRoute = ({ action, component: Component, ...routeProps }) => {
+    const WrapperComponent = (componentProps) => {
+        action(componentProps.match)
+        return <Component {...componentProps} />
+    }
+    return < Route {...routeProps} component={WrapperComponent} />
+}
+
+const CRRoute = connect(null, { action: match => ({ type: 'ROUTE', match }) })(RRoute)
+
+const ProtectedRoute = ({
+    fallback = '/',
+    roles = ["admin"],
+    auth,
+    component: Component,
+    ...routeProps
+}) => {
+    const WrapperComponent = ({ componentProps }) => {
+        const acl = auth?.payload?.sub?.acl || ['anon']
+        const newArr = roles.filter(r => acl.includes(r))
+        return newArr.length !== 0 ? < Component {...componentProps} /> : <Redirect to='/' />
+
+    }
+    return <CRRoute {...routeProps} component={WrapperComponent} />
+}
+
+export const CPRoute = connect(state => ({ auth: state.auth }))(ProtectedRoute)
+
+const queries = {
+    "/good/:_id": match => ({
+        name: 'goodById',
+        query: `query goodById($q: String) {
+                                        GoodFindOne(query: $q) {
+                                            _id name price description images {
+                                            url
+                                            }
+                                        }
+                                    }`,
+        variables: { q: JSON.stringify([{ _id: match.params._id }]) }
+    })
+}
+
+
+export const actionGoodById = ({ match }) =>
+    async dispatch => {
+        console.log(match)
+        if (match.path in queries) {
+            const { name, query, variables } = queries[match.path](match)
+            await dispatch(actionPromise(name, gql(query, variables)))
+        }
+    }
+
 
 store.subscribe(() => console.log(store.getState()))
 store.dispatch(actionRootCats())
 
 export const history = createHistory()
 
+export const actionAboutMe = (_id) =>
+    async dispatch => {
+        await dispatch(actionPromise('dataProfileAuth', gql(`query userOned($id:String){
+                        UserFindOne(query: $id){
+                            _id  
+                        
+                            avatar { _id url }
+                           
+                  }
+                }`, { id: JSON.stringify([{ _id }]) })))
+    }
+
+const actionUploadFile = (file) => {
+    let fd = new FormData()
+    fd.append('photo', file)
+    return actionPromise('upload', fetch(`${backURL}/upload`, {
+        method: "POST",
+        headers: localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {},
+        body: fd
+    }).then(res => res.json()))
+}
+
+const actionSetAvatar = file =>
+    async (dispatch, getState) => {
+        const result = await dispatch(actionUploadFile(file))
+        if (result) {
+            const { auth: { payload: { sub: { id } } } } = getState()
+            console.log(result);
+            await dispatch(actionPromise('uploadPhoto', gql(`mutation setUserAvatar($avar: UserInput){
+    UserUpsert(user: $avar){
+        _id avatar {_id}
+    }
+}`, { avar: { _id: id, avatar: { _id: result._id } } }))
+            )
+            await dispatch(actionAboutMe(id))
+        }
+    }
+
+
+function MyDropzone({ onUpload }) {
+    const onDrop = useCallback(acceptedFiles => {
+        onUpload(acceptedFiles[0])
+    }, [])
+    const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
+
+    return (
+        <div {...getRootProps()}>
+            <input {...getInputProps()} />
+            {
+                isDragActive ?
+                    <p>Drop the files here ...</p> :
+                    <p>Drag 'n' drop some files here, or click to select files</p>
+            }
+        </div>
+    )
+}
+
+
+
+
+
+const useLocalStoredState = (defaultState, localStorageName) => {
+    let inputJson
+    try {
+        inputJson = JSON.parse(localStorage.getItem(localStorageName))
+    } catch (err) {
+        console.error(err)
+        localStorage.removeItem(localStorageName)
+    }
+    const [state, setState] = useState(inputJson || defaultState)
+
+    return [state, newState => {
+        setState(newState)
+        localStorage.setItem(localStorageName, JSON.stringify(newState))
+    }]
+}
+
+
+
+const CMyDropzone = connect(null, { onUpload: actionSetAvatar })(MyDropzone)
+
+
 function App() {
     return (
         <Router history={history}>
             <Provider store={store}>
                 <div className="App">
+                    <CMyDropzone />
                     <Header />
                     <Main />
                     <Footer />

+ 5 - 4
src/components/main/Main.js

@@ -1,9 +1,10 @@
-import { CPageCategory } from './category/Category'
+import { PageCategory } from './category/Category'
 import { Route } from 'react-router-dom';
 import { Aside } from '../aside/Aside';
 import { CCart } from './cart/Cart';
-import { CPageGood } from './good/PageGood';
+import { PageGood } from './good/PageGood';
 import { CDashboard } from './dashboard/Dashboard';
+import { CPRoute } from '../../App';
 
 const Content = ({ children }) =>
     <div className='Content'>
@@ -20,8 +21,8 @@ export const Main = () =>
         <Aside />
         <Content>
             <Route path='/' component={PageMain} exact />
-            <Route path='/category/:_id' component={CPageCategory} />
-            <Route path='/good/:_id' component={CPageGood} />
+            <CPRoute roles={['anon', 'user', 'admin']} path="/category/:_id" component={PageCategory} />
+            <CPRoute roles={['anon', 'user', 'admin']} path="/good/:_id" component={PageGood} />
             <Route path='/order' component={CCart} />
             <Route path='/dashboard' component={CDashboard} />
         </Content>

+ 1 - 9
src/components/main/category/Category.js

@@ -15,13 +15,5 @@ const Category = ({ cat: { _id, name, goods, subCategories } = {} }) =>
 
 const CCategory = connect(state => ({ cat: state.promise.catById?.payload }))(Category)
 
-export const PageCategory = ({ match: { params: { _id } }, getData }) => {
-    useEffect(() => {
-        getData(_id)
-    }, [_id])
-    return (
+export const PageCategory = () => 
         <CCategory />
-    )
-}
-
-export const CPageCategory = connect(null, { getData: actionCatById })(PageCategory)

+ 4 - 8
src/components/main/good/PageGood.js

@@ -1,17 +1,13 @@
-import { connect } from 'react-redux';
-import { useEffect } from "react"
-import { actionGoodById } from '../../../redux-Store/redux-Store'
+
 import { CGood } from './Good';
 
-export const PageGood = ({ match: { params: { _id } }, getGood }) => {
-    useEffect(() => {
-        getGood(_id)
-    }, [_id])
+export const PageGood = () => {
+
     return (
         <><CGood /></>
     )
 }
 
-export const CPageGood = connect(null, { getGood: actionGoodById })(PageGood)
+
 
 

+ 1 - 1
src/redux-Store/promiseReducer.js

@@ -20,7 +20,7 @@ export function promiseReducer(state = {}, { type, status, payload, error, name
     if (type === 'PROMISE') {
         return {
             ...state,
-            [name]: { status, payload, error }
+            [name]: { status, payload: (status === 'PENDING' && state[name] && state[name].payload) || payload, error }
         }
     }
     return state;

+ 21 - 11
src/redux-Store/redux-Store.js

@@ -4,6 +4,7 @@ import { authReducer } from './authReducer'
 import { promiseReducer } from './promiseReducer'
 import { cartReducer } from './cartReducer'
 import { actionPromise } from './promiseReducer';
+import { routeReducer } from '../App';
 
 export const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
 
@@ -20,14 +21,6 @@ export const actionCatById = (_id) =>
         }`, { q: JSON.stringify([{ _id }]) }))
 
 
-export const actionGoodById = (_id) =>
-    actionPromise('goodById', gql(`query goodByID($q: String) {
-                                        GoodFindOne(query: $q){
-                                            name _id  description price images{url}
-                                        }
-        }`, {
-        q: JSON.stringify([{ _id }])
-    }))
 
 
 export const actionMyOrders = () =>
@@ -56,9 +49,26 @@ const getGQL = url =>
 
 export const gql = getGQL(backURL + '/graphql');
 
+const localStorageReducer = (reducer, localStorageName) =>
+    (state, action) => {
+        if (!state && !!localStorage[localStorageName]) {
+            let local = JSON.parse(localStorage[localStorageName])
+            return local
+
+        } else {
+            let newState = reducer(state, action)
+            localStorage[localStorageName] = JSON.stringify(newState)
+            return newState
+        }
+    }
+
 export const store = createStore(combineReducers({
-    promise: promiseReducer,
+    promise: localStorageReducer(promiseReducer, 'promise'),
+    cart: localStorageReducer(cartReducer, 'cart'),
     auth: authReducer,
-    cart: cartReducer
+    route: routeReducer,
 }),
-    applyMiddleware(thunk))
+    applyMiddleware(thunk))
+
+
+

+ 21 - 0
yarn.lock

@@ -2263,6 +2263,11 @@ atob@^2.1.2:
   resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
   integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
 
+attr-accept@^2.2.1:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
+  integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
+
 autoprefixer@^10.4.0:
   version "10.4.0"
   resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.0.tgz#c3577eb32a1079a440ec253e404eaf1eb21388c8"
@@ -3983,6 +3988,13 @@ file-loader@^6.2.0:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"
 
+file-selector@^0.2.2:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80"
+  integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==
+  dependencies:
+    tslib "^2.0.3"
+
 filelist@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b"
@@ -6979,6 +6991,15 @@ react-dom@^17.0.2:
     object-assign "^4.1.1"
     scheduler "^0.20.2"
 
+react-dropzone@^11.5.1:
+  version "11.5.1"
+  resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.5.1.tgz#f4d664437bf8af6acfccbf5040a9890c6780a49f"
+  integrity sha512-eNhttdq4ZDe3eKbXAe54Opt+sbtqmNK5NWTHf/l5d+1TdZqShJ8gMjBrya00qx5zkI//TYxRhu1d9pemTgaWwg==
+  dependencies:
+    attr-accept "^2.2.1"
+    file-selector "^0.2.2"
+    prop-types "^15.7.2"
+
 react-error-overlay@^6.0.10:
   version "6.0.10"
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"