Parcourir la source

add actions +login_page +store

kurkabein il y a 2 ans
Parent
commit
628d400fa9

+ 118 - 0
package-lock.json

@@ -11,13 +11,16 @@
         "@testing-library/jest-dom": "^5.16.2",
         "@testing-library/react": "^12.1.2",
         "@testing-library/user-event": "^13.5.0",
+        "array-move": "^4.0.0",
         "node-sass": "^7.0.1",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
+        "react-dropzone": "^12.0.2",
         "react-icons": "^4.3.1",
         "react-redux": "^7.2.6",
         "react-router-dom": "^5.3.0",
         "react-scripts": "5.0.0",
+        "react-sortable-hoc": "^2.0.0",
         "redux": "^4.1.2",
         "redux-thunk": "^2.4.1",
         "web-vitals": "^2.1.4"
@@ -4383,6 +4386,17 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/array-move": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
+      "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -4497,6 +4511,14 @@
         "node": ">= 4.5.0"
       }
     },
+    "node_modules/attr-accept": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+      "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.2",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
@@ -7647,6 +7669,17 @@
         "webpack": "^4.0.0 || ^5.0.0"
       }
     },
+    "node_modules/file-selector": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
+      "integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==",
+      "dependencies": {
+        "tslib": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/filelist": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
@@ -8823,6 +8856,14 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "node_modules/ip": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@@ -14100,6 +14141,22 @@
         "react": "17.0.2"
       }
     },
+    "node_modules/react-dropzone": {
+      "version": "12.0.2",
+      "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.2.tgz",
+      "integrity": "sha512-wnU3+QZA9H5XqC+UNdEDs5YuB+XgJh5mQ9Bop4PUtN/2nKTGTpBbfkcsm9OVCYylBHak69Ezjzbhx/0Cb6OfjA==",
+      "dependencies": {
+        "attr-accept": "^2.2.2",
+        "file-selector": "^0.4.0",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">= 10.13"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8"
+      }
+    },
     "node_modules/react-error-overlay": {
       "version": "6.0.10",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
@@ -14277,6 +14334,21 @@
         }
       }
     },
+    "node_modules/react-sortable-hoc": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
+      "integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
+      "dependencies": {
+        "@babel/runtime": "^7.2.0",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.5.7"
+      },
+      "peerDependencies": {
+        "prop-types": "^15.5.7",
+        "react": "^16.3.0 || ^17.0.0",
+        "react-dom": "^16.3.0 || ^17.0.0"
+      }
+    },
     "node_modules/read-pkg": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -20701,6 +20773,11 @@
         "is-string": "^1.0.7"
       }
     },
+    "array-move": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
+      "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ=="
+    },
     "array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -20782,6 +20859,11 @@
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
       "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
     },
+    "attr-accept": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+      "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
+    },
     "autoprefixer": {
       "version": "10.4.2",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
@@ -23123,6 +23205,14 @@
         "schema-utils": "^3.0.0"
       }
     },
+    "file-selector": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
+      "integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==",
+      "requires": {
+        "tslib": "^2.0.3"
+      }
+    },
     "filelist": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
@@ -23977,6 +24067,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",
@@ -27673,6 +27771,16 @@
         "scheduler": "^0.20.2"
       }
     },
+    "react-dropzone": {
+      "version": "12.0.2",
+      "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.2.tgz",
+      "integrity": "sha512-wnU3+QZA9H5XqC+UNdEDs5YuB+XgJh5mQ9Bop4PUtN/2nKTGTpBbfkcsm9OVCYylBHak69Ezjzbhx/0Cb6OfjA==",
+      "requires": {
+        "attr-accept": "^2.2.2",
+        "file-selector": "^0.4.0",
+        "prop-types": "^15.8.1"
+      }
+    },
     "react-error-overlay": {
       "version": "6.0.10",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
@@ -27813,6 +27921,16 @@
         "workbox-webpack-plugin": "^6.4.1"
       }
     },
+    "react-sortable-hoc": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
+      "integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
+      "requires": {
+        "@babel/runtime": "^7.2.0",
+        "invariant": "^2.2.4",
+        "prop-types": "^15.5.7"
+      }
+    },
     "read-pkg": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",

+ 3 - 0
package.json

@@ -6,13 +6,16 @@
     "@testing-library/jest-dom": "^5.16.2",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "array-move": "^4.0.0",
     "node-sass": "^7.0.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
+    "react-dropzone": "^12.0.2",
     "react-icons": "^4.3.1",
     "react-redux": "^7.2.6",
     "react-router-dom": "^5.3.0",
     "react-scripts": "5.0.0",
+    "react-sortable-hoc": "^2.0.0",
     "redux": "^4.1.2",
     "redux-thunk": "^2.4.1",
     "web-vitals": "^2.1.4"

+ 18 - 5
src/App.js

@@ -1,17 +1,30 @@
 
 import './App.scss';
 import Header from './components/Header';
-import Main from './components/Main';
-import {Router} from 'react-router-dom';
+import Main from './components/MainPage/Main';
+import CLoginForm from './components/LoginPage/Login';
+import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
 import createHistory from "history/createBrowserHistory";
-const history = createHistory()
+import { Provider } from 'react-redux';
+import store from './reducers';
+const history = createHistory();
+
+
 function App() {
   return (
     <Router history={history}>
+      <Provider store={store}>
+      <Switch>
     <div className="App">
-      <Header/>
-      <Main/>
+        <Route exact path="/login" component={CLoginForm}/>
+        <Route exact path="/" component={Header}/>
+        <Route exact path="/" component={Main}/>
+        <Route exact path="/collections" component={Header}/>
+        <Route exact path="/collections" component={Main}/>
+        <Route exact path="/createpost" component={Main}/>
     </div>
+    </Switch>
+    </Provider>
     </Router>
   );
 }

+ 7 - 2
src/App.scss

@@ -1,6 +1,11 @@
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
-.Header {
-  
+
+.LoginInput{
+    @apply appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-fuchsia-500 focus:border-fuchsia-500 focus:z-10 sm:text-sm #{!important};
+}
+
+.LoginButton {
+    @apply  relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-fuchsia-600 hover:bg-fuchsia-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-fuchsia-500 #{!important};
 }

+ 60 - 0
src/actions/index.js

@@ -0,0 +1,60 @@
+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://hipstagram.asmer.fs.a-level.com.ua';
+
+const gql = getGQL(backendURL + '/graphql');
+
+
+
+export const actionPromise = (name,promise) =>{
+ const actionPending = name => ({name,type:"PROMISE",status:'PENDING'})
+ const actionFulfiled = (name,payload) =>({name,type:"PROMISE",status:'FULFILLED',payload})
+ const actionRejected = (name,error) => ({name,type:"PROMISE",status:'REJECTED',error})
+
+
+ return async dispatch => {
+    dispatch(actionPending(name))
+    try{
+        let payload = await promise
+        dispatch(actionFulfiled(name,payload))
+        return payload
+    }
+    catch(e){
+        dispatch(actionRejected(name,e))
+    }
+ }
+}
+
+export const actionAuthLogin = (tokennn) =>({type:'AUTH_LOGIN',token:tokennn})
+export const actionAuthLogout= () => ({type: 'AUTH_LOGOUT'})
+
+
+export 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}))
+
+export 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));
+}

src/components/Collections.js → src/components/CollectionsPage/Collections.js


+ 145 - 0
src/components/CreatePostPage/UpsertPost.js

@@ -0,0 +1,145 @@
+import { useDropzone } from 'react-dropzone';
+import {arrayMoveImmutable} from 'array-move';
+import {sortableContainer, sortableElement} from 'react-sortable-hoc';
+import { useState } from 'react';
+
+const backendURL = 'http://hipstagram.asmer.fs.a-level.com.ua';
+
+
+
+
+
+
+function MyDropZone({props, onLoad}) {
+    const {acceptedFiles, getRootProps, getInputProps} = useDropzone();
+    /* useEffect(() => {
+      if(acceptedFiles[0]){
+          onLoad(acceptedFiles[0])
+      }
+    },[acceptedFiles[0]]) */
+    const files = acceptedFiles.map(file => (
+      <li key={file.path}>
+        {file.path} - {file.size} bytes
+      </li>
+    ));
+  
+    return (
+      <section className="container">
+        <div {...getRootProps({className: 'dropzone'})}>
+          <input {...getInputProps()} />
+          <p>Drag 'n' drop some files here, or click to select files</p>
+        </div>
+        <aside>
+          <h4>Files</h4>
+          <ul>{files}</ul>
+        </aside>
+      </section>
+    );
+  }
+  
+
+
+let defaultPost = [{
+  "_id": "5d6d9aa25bc6e90a3ba1afe2",
+  "title": null,
+  "text": "aaa",
+  "images": [
+    "images/1f8973225107076a4e2f2e4ac5e35e74"
+   ,
+    "images/d766a6cb2d3c232f3e9241abf0751cbc"
+   ,
+    "images/b3def30bab7249af56ef341536327fd3"
+   
+ ],
+ "owner": {
+   "_id": "5d66e01dc6a7071408ac1e1c",
+   "nick": null
+ } 
+}]
+
+const defImg = {images:[{url:"images/b3def30bab7249af56ef341536327fd3"},{url:"images/d766a6cb2d3c232f3e9241abf0751cbc"},{url:"images/1f8973225107076a4e2f2e4ac5e35e74"}]}
+
+const Photo = ({photo}) => {
+  return (
+    <img src={`${backendURL}+"/"+${photo}`}/>
+  )
+}
+
+
+
+
+
+  const defitems = {items:['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6']};
+
+
+
+  const EntityEditor = ({entity={images: []}, onSave, onFileDrop, fileStatus}) => {
+    const [state,setState] = useState(entity)
+    //по файлу в дропзоне:
+        //дергать onFileDrop
+        //fileStatus - информация о заливке файла из redux
+        //через useEffect дождаться когда файл зальется
+        //и сделать setState({...state, array: [...state.array, {объект файла с бэка с _id и url}]})
+    //по react-sortable-hoc
+        //делаете как в пример arrayMove для state.array
+    //по кнопке сохранения делаем onSave(state)
+    //где-то рядом остальные поля из state типа title name text
+    //но это вы уже знаете
+
+    const SortableItem = sortableElement(({value}) => <div>
+    {/* <img src={`${backendURL}+"/"${value.images[0]}`} alt="photo"/> */}
+    <h2>{value._id}</h2>
+    <p>{value.text}</p>
+</div>);
+
+    const SortableContainer = sortableContainer(({children}) => {
+  return <div>{children}</div>;
+});
+
+
+    const  onSortEnd = ({oldIndex, newIndex}) => {
+        console.log(state, oldIndex, newIndex, arrayMoveImmutable(state.images, oldIndex, newIndex));
+        setState((state) => {
+            return {images: arrayMoveImmutable(state.images, oldIndex, newIndex)}
+          })
+        /* setState((state) => ({
+            items: arrayMoveImmutable(state.items, oldIndex, newIndex),
+          })); */
+    }
+    // console.log(state)
+            
+    return(
+        <SortableContainer onSortEnd={onSortEnd}>
+        {state.images.map((value, index) => (
+          <SortableItem key={`item-${value}`} index={index} value={value} />
+        ))}
+      </SortableContainer>
+    )
+
+}
+
+
+
+
+
+ const UpsertPost = () => {
+    /*  const [state, setState] = useState({...items});
+
+     const  onSortEnd = ((oldIndex, newIndex) => {
+        setState((state) => ({
+            items: arrayMoveImmutable(state.items, oldIndex, newIndex),
+          }));
+    }) */
+
+    return(
+       /*  <SortableContainer onSortEnd={onSortEnd}>
+        {state.items.map((value, index) => (
+          <SortableItem key={`item-${value}`} index={index} value={value} />
+        ))}
+      </SortableContainer> */
+      <EntityEditor entity={defaultPost}/>
+    )
+        }
+
+
+export default UpsertPost;

+ 38 - 0
src/components/LoginPage/Login.js

@@ -0,0 +1,38 @@
+import { useState } from "react";
+import { connect } from "react-redux";
+import { actionFullLogin } from "../../actions";
+import {Link} from "react-router-dom"
+
+const Login = ({onLogin}) => {
+       const[login,setLogin] = useState('');
+       const[password,setPassword] = useState(''); 
+
+    return (
+        <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
+                <div className="max-w-md w-full space-y-8">
+                        <div>
+                            <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
+                        </div>
+                        <div className="mt-8 space-y-6 rounded-md shadow-sm ">
+                            <div>
+                            <label>Login:</label>
+                            <input placeholder="login" value={login} type="text" className="LoginInput" onChange={event => setLogin(event.target.value)}/>
+                            </div>
+                            <div>
+                            <label>Password:</label>
+                            <input placeholder="password" value={password} type="password" className="LoginInput" onChange={event => setPassword(event.target.value)}/>
+                            </div>
+                            <div className="text-gray-400">
+                            You dont have account? 
+                                <Link  className="text-gray-600 hover:underline hover:text-fuchsia-600" to="/register"> Create!</Link>
+                            </div>
+                           <button className="LoginButton" onClick={()=> onLogin(login,password)}>Login</button>
+                        </div>
+                </div>
+        </div>
+    )
+}
+
+const CLoginForm = connect(null, {onLogin: actionFullLogin})(Login)
+
+export default CLoginForm;

+ 5 - 3
src/components/Main.js

@@ -1,7 +1,8 @@
 import {Route,Redirect, Switch} from 'react-router-dom';
-import Feed from './Feed/Feed';
-import User from './User';
-import Collections from './Collections';
+import Feed from '../Feed/Feed';
+import User from '../UserPage/User';
+import Collections from '../CollectionsPage/Collections';
+import UpsertPost from '../CreatePostPage/UpsertPost';
 const Main = () => {
 
 
@@ -12,6 +13,7 @@ const Main = () => {
                 <Route path="/" exact component={Feed}/>
                 <Route path="/user/:id" exact component={User}/>
                 <Route path="/collections" exact component={Collections}/>
+                <Route path="/createpost" exact component={UpsertPost}/>
             </Switch>
         </div>
     )

+ 0 - 0
src/components/RegisterPage/Register.js


src/components/User.js → src/components/UserPage/User.js


+ 57 - 0
src/reducers/index.js

@@ -0,0 +1,57 @@
+import {createStore, applyMiddleware, combineReducers} from 'redux';
+import thunk from 'redux-thunk';
+
+
+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
+  }
+ 
+
+  
+  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 || {};
+  }
+  
+  const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer}), applyMiddleware(thunk))
+  store.subscribe(() => console.log(store.getState()));
+  
+export default store;
+