From a69de48424b6eb1bf383d4d196c895978a4cf015 Mon Sep 17 00:00:00 2001 From: Isra O Date: Tue, 19 Jan 2021 00:15:00 -0500 Subject: [PATCH 1/6] redux-add --- debug.log | 5 ++ package.json | 1 + src/actions/auth.js | 26 +++++++ src/actions/types.js | 2 + src/components/Navbar/Navbar.js | 8 +- src/components/UserDropdownMenu/MenuData.js | 8 +- .../UserDropdownMenu/UserDropdownMenu.jsx | 12 +-- src/components/auth/FormSuccess.js | 11 --- src/components/auth/Login.js | 69 +++++++++++++++-- src/components/auth/SignUp.js | 4 +- src/components/auth/SignUpForm.js | 74 +++++++++++-------- src/components/auth/Submit.js | 18 ----- src/components/auth/useForm.js | 37 ---------- src/components/auth/validateInfo.js | 25 ------- src/reducers/auth.js | 35 +++++++++ src/reducers/index.js | 6 ++ src/store.js | 1 + yarn.lock | 36 ++++----- 18 files changed, 211 insertions(+), 167 deletions(-) create mode 100644 src/actions/auth.js create mode 100644 src/actions/types.js delete mode 100644 src/components/auth/FormSuccess.js delete mode 100644 src/components/auth/Submit.js delete mode 100644 src/components/auth/useForm.js delete mode 100644 src/components/auth/validateInfo.js create mode 100644 src/reducers/auth.js create mode 100644 src/reducers/index.js diff --git a/debug.log b/debug.log index 8546a45..12ad379 100644 --- a/debug.log +++ b/debug.log @@ -4,3 +4,8 @@ [1204/213558.930:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) [1207/172622.290:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) [1209/184337.106:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[0114/103852.805:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[0115/111046.673:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[0116/111049.168:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[0117/111049.570:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[0118/111049.891:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) diff --git a/package.json b/package.json index 1a72838..f0c49ab 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "axios": "^0.21.1", "husky": "^4.3.0", "lodash": "^4.17.20", "moment": "^2.29.1", diff --git a/src/actions/auth.js b/src/actions/auth.js new file mode 100644 index 0000000..1c88ca6 --- /dev/null +++ b/src/actions/auth.js @@ -0,0 +1,26 @@ +import axios from "axios"; +import { REGISTER_SUCCSESS, REGISTER_FAIL } from "./types"; + +export const register = ({ username, email, password }) => async (dispatch) => { + const config = { + headers: { + content_Type: "application/json", + }, + }; + + const body = JSON.stringify({ username, email, password }); + + try { + const res = await axios.post("api/users", body, config); + dispatch({ + type: REGISTER_SUCCSESS, + payload: res.data, + }); + } catch (err) { + const errors = err.response.data.errors; + + dispatch({ + type: REGISTER_FAIL, + }); + } +}; diff --git a/src/actions/types.js b/src/actions/types.js new file mode 100644 index 0000000..a30e083 --- /dev/null +++ b/src/actions/types.js @@ -0,0 +1,2 @@ +export const REGISTER_SUCCSESS = "REGISTER_SUCCSESS"; +export const REGISTER_FAIL = "REGISTER_FAIL"; diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js index ba5cacf..7fd1ada 100644 --- a/src/components/Navbar/Navbar.js +++ b/src/components/Navbar/Navbar.js @@ -14,13 +14,9 @@ export const Navbar = ({ signedIn }) => { ) : ( -
- Sign Up - - - My Classes + Sign up/Login
); @@ -33,7 +29,7 @@ export const Navbar = ({ signedIn }) => { -
+
diff --git a/src/components/UserDropdownMenu/MenuData.js b/src/components/UserDropdownMenu/MenuData.js index 1d03e65..0649bdc 100644 --- a/src/components/UserDropdownMenu/MenuData.js +++ b/src/components/UserDropdownMenu/MenuData.js @@ -15,9 +15,15 @@ export const MenuData = [ icon: , }, + { + title: "My Learning", + path: "/my-classes", + icon: , + }, + { title: "Log Out", - path: "/#", + path: "/", icon: , }, ]; diff --git a/src/components/UserDropdownMenu/UserDropdownMenu.jsx b/src/components/UserDropdownMenu/UserDropdownMenu.jsx index 8b84d5d..52a0820 100644 --- a/src/components/UserDropdownMenu/UserDropdownMenu.jsx +++ b/src/components/UserDropdownMenu/UserDropdownMenu.jsx @@ -12,7 +12,6 @@ function UserDropdownMenu() { return (
showMenu()} onMouseLeave={() => showMenu()} @@ -25,27 +24,22 @@ function UserDropdownMenu() { -
diff --git a/src/components/auth/FormSuccess.js b/src/components/auth/FormSuccess.js deleted file mode 100644 index 90cd43e..0000000 --- a/src/components/auth/FormSuccess.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -const FormSuccess = () => { - return ( -
-

We have received your request!

-
- ); -}; - -export default FormSuccess; diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js index 38f6e58..c022408 100644 --- a/src/components/auth/Login.js +++ b/src/components/auth/Login.js @@ -1,11 +1,66 @@ -import React from 'react' +import React, { useState } from "react"; +import { Link } from "react-router-dom"; -function Login() { - return ( +const Login = () => { + const [formData, setFormData] = useState({ + email: "", + password: "", + }); + + const { name, email, password } = formData; + + const onChange = (e) => + setFormData({ ...formData, [e.target.name]: e.target.value }); + + const onSubmit = async (e) => { + e.preventDefault(); + console.log("success"); + }; + + return ( +
+

Sign In

+

+ Sign into Your Account +

+
onSubmit(e)}> +
+ onChange(e)} + required + /> +
- Login + onChange(e)} + className="w-100 h2 mv2 f4" + required + />
- ) -} + +
+

+ Don't have an account?{" "} + + Sign Up + +

+
+ ); +}; -export default Login \ No newline at end of file +export default Login; diff --git a/src/components/auth/SignUp.js b/src/components/auth/SignUp.js index 504e7a8..f61618b 100644 --- a/src/components/auth/SignUp.js +++ b/src/components/auth/SignUp.js @@ -1,10 +1,10 @@ import React from "react"; -import Submit from "./Submit"; +import SignUpForm from "./SignUpForm"; export default function SignUp() { return (
- +
); } diff --git a/src/components/auth/SignUpForm.js b/src/components/auth/SignUpForm.js index e65dfd0..fa71b0b 100644 --- a/src/components/auth/SignUpForm.js +++ b/src/components/auth/SignUpForm.js @@ -1,13 +1,31 @@ -import React from "react"; +import React, { useState } from "react"; +import { connect } from "react-redux"; import { Link } from "react-router-dom"; -import useForm from "./useForm"; -import validate from "./validateInfo"; +import { register } from "../../actions/auth"; +import PropTypes from "prop-types"; -const SignUpForm = ({ submitForm }) => { - const { handleChange, values, handleSubmit, errors } = useForm( - submitForm, - validate - ); +const SignUpForm = ({ register }) => { + const [formData, setFormData] = useState({ + username: "", + email: "", + password: "", + password2: "", + }); + + const { username, email, password, password2 } = formData; + + const onChange = (e) => + setFormData({ ...formData, [e.target.name]: e.target.value }); + + const onSubmit = async (e) => { + e.preventDefault(); + + if (password !== password2) { + console.log("passwords don't match"); + } else { + register({ username, email, password }); + } + }; return (

Sign Up

@@ -15,18 +33,16 @@ const SignUpForm = ({ submitForm }) => { Create Your Account

-
+ onSubmit(e)}>
onChange(e)} /> - - {errors.name &&

{errors.name}

}
@@ -35,10 +51,9 @@ const SignUpForm = ({ submitForm }) => { placeholder="Email Address" name="email" className="w-100 h2 f4 mv2" - value={values.email} - onChange={handleChange} + value={email} + onChange={(e) => onChange(e)} /> - {errors.email &&

{errors.email}

}
{ placeholder="Password" name="password" className="w-100 h2 f4 mv2" - value={values.password} - onChange={handleChange} + value={password} + onChange={(e) => onChange(e)} /> - {errors.password &&

{errors.password}

}
{ placeholder="Confirm Password" name="password2" className="w-100 f4 h2 mv2" - value={values.password2} - onChange={handleChange} + value={password2} + onChange={(e) => onChange(e)} /> - {errors.password2 &&

{errors.password2}

}
- + value="Register" + />

- Already have an account?{" "} + Already have an account? Sign In @@ -80,4 +92,8 @@ const SignUpForm = ({ submitForm }) => { ); }; -export default SignUpForm; +SignUpForm.propTypes = { + register: PropTypes.func.isRequired, +}; + +export default connect(null, { register })(SignUpForm); diff --git a/src/components/auth/Submit.js b/src/components/auth/Submit.js deleted file mode 100644 index 5e45a0d..0000000 --- a/src/components/auth/Submit.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useState } from "react"; -import FormSuccess from "./FormSuccess"; -import SignUpForm from "./SignUpForm"; - -const Submit = () => { - const [isSubmitted, setIsSubmitted] = useState(false); - - function submitForm() { - setIsSubmitted(true); - } - return ( - <> - {!isSubmitted ? : } - - ); -}; - -export default Submit; diff --git a/src/components/auth/useForm.js b/src/components/auth/useForm.js deleted file mode 100644 index 01dd00d..0000000 --- a/src/components/auth/useForm.js +++ /dev/null @@ -1,37 +0,0 @@ -import { useState, useEffect } from "react"; - -const useForm = (callback, validate) => { - const [values, setValues] = useState({ - name: "", - email: "", - password: "", - password2: "", - }); - - const [errors, setErrors] = useState({}); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleChange = (e) => { - const { name, value } = e.target; - setValues({ - ...values, - [name]: value, - }); - }; - - const handleSubmit = (e) => { - e.preventDefault(); - setErrors(validate(values)); - setIsSubmitting(true); - }; - - useEffect(() => { - if (Object.keys(errors).length === 0 && isSubmitting) { - callback(); - } - }, [errors]); - - return { handleChange, values, handleSubmit, errors }; -}; - -export default useForm; diff --git a/src/components/auth/validateInfo.js b/src/components/auth/validateInfo.js deleted file mode 100644 index 1a26e10..0000000 --- a/src/components/auth/validateInfo.js +++ /dev/null @@ -1,25 +0,0 @@ -export default function validateInfo(values) { - let errors = {}; - - if (!values.name.trim()) { - errors.name = "Username required"; - } - - if (!values.email) { - errors.email = "Email required"; - } else if (!/\S+@\S+\.\S+/.test(values.email)) { - errors.email = "Email address is invalid"; - } - if (!values.password) { - errors.password = "Password is required"; - } else if (values.password.length < 6) { - errors.password = "Password needs to be 6 characters or more"; - } - - if (!values.password2) { - errors.password2 = "Password is required"; - } else if (values.password2 !== values.password) { - errors.password2 = "Passwords do not match"; - } - return errors; -} diff --git a/src/reducers/auth.js b/src/reducers/auth.js new file mode 100644 index 0000000..52763dd --- /dev/null +++ b/src/reducers/auth.js @@ -0,0 +1,35 @@ +import { REGISTER_SUCCSESS, REGISTER_FAIL } from "../actions/types"; + +const initialState = { + token: localStorage.getItem("getItem"), + isAuthenicated: null, + loading: true, + user: null, +}; + +export default function (state = initialState, action) { + const { type, payload } = action; + + switch (type) { + case REGISTER_SUCCSESS: + localStorage.setItem("token", payload.token); + return { + ...state, + ...payload, + isAuthenicated: true, + loading: false, + }; + + case REGISTER_FAIL: + localStorage.removeItem("token"); + return { + ...state, + token: null, + isAuthenicated: false, + loading: false, + }; + + default: + return state; + } +} diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 0000000..3df0884 --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,6 @@ +import { combineReducers } from "redux"; +import auth from "./auth"; + +export default combineReducers({ + auth, +}); diff --git a/src/store.js b/src/store.js index 53dcce5..c522a05 100644 --- a/src/store.js +++ b/src/store.js @@ -5,6 +5,7 @@ import { apiFactory } from "./lib/api"; import thunk from "redux-thunk"; import { applyMiddleware, compose, createStore } from "redux"; import promise from "redux-promise-middleware"; +import rootReducer from "./reducers"; // Add api endpoints here const apiStateMap = { diff --git a/yarn.lock b/yarn.lock index 7b9e36c..cf63e3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1319,16 +1319,6 @@ schema-utils "^2.6.5" source-map "^0.7.3" -"@reduxjs/toolkit@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.5.0.tgz#1025c1ccb224d1fc06d8d98a61f6717d57e6d477" - integrity sha512-E/FUraRx+8guw9Hlg/Ja8jI/hwCrmIKed8Annt9YsZw3BQp+F24t5I5b2OWR6pkEHY4hn1BgP08FrTZFRKsdaQ== - dependencies: - immer "^8.0.0" - redux "^4.0.0" - redux-thunk "^2.3.0" - reselect "^4.0.0" - "@rollup/plugin-node-resolve@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca" @@ -2396,6 +2386,13 @@ axe-core@^3.5.4: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== + dependencies: + follow-redirects "^1.10.0" + axobject-query@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -5033,6 +5030,11 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== +follow-redirects@^1.10.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" + integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -5730,11 +5732,6 @@ immer@7.0.9: resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.9.tgz#28e7552c21d39dd76feccd2b800b7bc86ee4a62e" integrity sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A== -immer@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.0.tgz#08763549ba9dd7d5e2eb4bec504a8315bd9440c2" - integrity sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg== - import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -9437,7 +9434,7 @@ react-scripts@4.0.0: optionalDependencies: fsevents "^2.1.3" -react@17.0.1: +react@17.0.1, react@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w== @@ -9575,7 +9572,7 @@ redux-thunk@^2.3.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== -redux@^4.0.0, redux@^4.0.5: +redux@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== @@ -9768,11 +9765,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reselect@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" - integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== - resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" From b9ee821bb36cc83135553c4b023da1cce13dea76 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Sun, 7 Feb 2021 16:10:08 -0700 Subject: [PATCH 2/6] using api --- src/components/auth/Login.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js index 4540750..eca3970 100644 --- a/src/components/auth/Login.js +++ b/src/components/auth/Login.js @@ -2,9 +2,10 @@ import React, { useState } from "react"; import { Link, Redirect } from "react-router-dom"; import { connect } from "react-redux"; import PropTypes from "prop-types"; -import { login } from "../../actions/auth"; +// import { login } from "../../actions/auth"; +import { actions, api } from "../../store"; -const Login = ({ login, isAuthenicated }) => { +const Login = ({ isAuthenticated }) => { const [formData, setFormData] = useState({ email: "", password: "", @@ -17,12 +18,15 @@ const Login = ({ login, isAuthenicated }) => { const onSubmit = async (e) => { e.preventDefault(); - login(email, password); + api.auth.post({ email, password }).then(({ payload }) => { + localStorage.setItem("token", payload.token); + actions.auth.update({ token: payload.token }); + }); }; //redirect if loggedin - //below line should be if(isAuthenicated) I am using ! for testing - if (!isAuthenicated) { + //below line should be if(isAuthenticated) I am using ! for testing + if (!isAuthenticated) { return ; } return ( @@ -73,10 +77,10 @@ const Login = ({ login, isAuthenicated }) => { Login.propTypes = { login: PropTypes.func.isRequired, - isAuthenicated: PropTypes.bool, + isAuthenticated: PropTypes.bool, }; const mapStateToProp = (state) => ({ - isAuthenicated: state.auth.isAuthenicated, + isAuthenticated: state.auth.isAuthenticated, }); -export default connect(mapStateToProp, { login })(Login); +export default connect(mapStateToProp)(Login); From d5d34483f13347be9c9790dae44da66c59597e25 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Sun, 7 Feb 2021 16:13:36 -0700 Subject: [PATCH 3/6] use api --- src/components/auth/SignUpForm.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/auth/SignUpForm.js b/src/components/auth/SignUpForm.js index a9c266d..5b2337d 100644 --- a/src/components/auth/SignUpForm.js +++ b/src/components/auth/SignUpForm.js @@ -1,9 +1,10 @@ import React, { useState } from "react"; -import { connect } from "react-redux"; +import { connect } from "../../lib/stateToRedux"; import { Link, Redirect } from "react-router-dom"; import { setAlert } from "../../actions/alert"; import { register } from "../../actions/auth"; import PropTypes from "prop-types"; +import { api } from "../../store"; const SignUpForm = ({ setAlert, register, isAuthenicated }) => { const [formData, setFormData] = useState({ @@ -24,7 +25,10 @@ const SignUpForm = ({ setAlert, register, isAuthenicated }) => { if (password !== password2) { setAlert("passwords don't match", "danger"); } else { - register({ username, email, password }); + api.user.create({ username, email, password }).then(({ data }) => { + console.log(data); + }); + // register({ username, email, password }); } }; @@ -105,7 +109,7 @@ SignUpForm.propTypes = { }; const mapStateToProp = (state) => ({ - isAuthenicated: state.auth.isAuthenicated, + isAuthenicated: state.auth.isAuthenticated, }); export default connect(mapStateToProp, { setAlert, register })(SignUpForm); From 24fcdbf0fc2a6143f4a2d85ec67af0f70738434d Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Sun, 7 Feb 2021 16:14:36 -0700 Subject: [PATCH 4/6] use nested api --- src/App.js | 7 ++----- src/components/Alert.js | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/App.js b/src/App.js index 70f5feb..42129a9 100644 --- a/src/App.js +++ b/src/App.js @@ -15,19 +15,16 @@ import Header from "./components/Header/Header"; import { api } from "./store"; import classPage from "./components/classPage/classPage"; import ScrollToTop from "./components/ScrollToTop"; -import { connect } from "react-redux"; import Login from "./components/auth/Login"; import SignUp from "./components/auth/SignUp"; import BreadCrumb from "./components/Breadcrumbs/Breadcrumbs"; import Cat from "./components/Cat"; -import Alert from "./components/Alert"; import { loadUser } from "./actions/auth"; import { store } from "./store"; import setAuthToken from "./utils/setAuthToken"; import Dashboard from "./components/dashboard/Dashboard"; -import PrivateRoute from "./components/PrivateRoute"; +import { connect } from "./lib/stateToRedux"; import CreateProfile from "./components/Profile/CreateProfile"; -import EditProfile from "./components/Profile/EditProfile"; if (localStorage.token) { setAuthToken(localStorage.token); @@ -129,7 +126,7 @@ function App(props) { } function mapStateToProps(state) { - return { categories: state.category, books: state.book }; + return { categories: state.api.category, books: state.api.book }; } export default connect(mapStateToProps)(App); diff --git a/src/components/Alert.js b/src/components/Alert.js index a507d76..ea03c8e 100644 --- a/src/components/Alert.js +++ b/src/components/Alert.js @@ -1,6 +1,6 @@ import React from "react"; import PropTypes from "prop-types"; -import { connect } from "react-redux"; +import { connect } from "../lib/stateToRedux"; const Alert = ({ alerts }) => alerts !== null && @@ -15,7 +15,7 @@ Alert.propTypes = { alerts: PropTypes.array.isRequired, }; const mapStateToProps = (state) => ({ - alerts: state.alert, + alerts: state.auth.alerts, }); export default connect(mapStateToProps)(Alert); From 7b09804f5df55c6a33ba3418cf9330daac855eb6 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Sun, 7 Feb 2021 16:24:45 -0700 Subject: [PATCH 5/6] adding ergonomic nested resources/namespacing --- src/lib/api.js | 31 +++++++------ src/lib/stateToRedux.js | 96 ++++++++++++++++++++++++++++++++--------- 2 files changed, 93 insertions(+), 34 deletions(-) diff --git a/src/lib/api.js b/src/lib/api.js index bae8255..a67dda9 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,9 +1,9 @@ -import { camelCase, isArray, keyBy, keys } from "lodash"; -import { fromStateMap } from "./stateToRedux"; +import { isArray, isEmpty, keyBy, keys } from "lodash"; import { crossProduct, trie } from "./utils"; const methods = { create: "post", + post: "post", read: "get", get: "get", update: "patch", @@ -27,11 +27,11 @@ const requestFactory = ( method, dispatch, actions, - cache + cache, + standardQuery ) => async (data, requestOptions = {}) => { - const { query } = requestOptions; - - const urlQuery = query + const query = { ...standardQuery, ...requestOptions.query }; + const urlQuery = !isEmpty(query) ? Object.entries(query) .map(([k, v]) => `${k}=${isArray(v) ? v.join(",") : v}`) .join("&") @@ -47,7 +47,9 @@ const requestFactory = ( method: ${method}`; } - const url = [baseURL, resourceName, id || ""].join("/") + "?" + urlQuery; + const url = + [baseURL, resourceName, id || ""].join("/") + + (urlQuery ? "?" + urlQuery : ""); if (!requestOptions.force && methods[method] === "get" && cache[url]) return; let options = { @@ -67,8 +69,10 @@ const requestFactory = ( return dispatch(requestAction).then(({ value }) => { cache[url] = true; isArray(value) && (value = keyBy(value, "id")); - const key = camelCase("update_" + resourceName); - return dispatch(actions[key](value)); + const update = resourceName + .split("/") + .reduce((future, token) => future[token], actions).update; + return update(value); }); }; @@ -97,10 +101,8 @@ const requestFactory = ( * * The tool does not work with nested resources. */ -export const apiFactory = (baseURL, stateMap, dispatch) => { - const { actions } = fromStateMap(stateMap); - - const resourceNames = keys(stateMap); +export const apiFactory = (baseURL, actions, dispatch, standardQuery) => { + const resourceNames = keys(actions); const cache = {}; // From [resource, method, function] triplets, we create a trie (https://en.wikipedia.org/wiki/Trie) const paths = crossProduct( @@ -115,7 +117,8 @@ export const apiFactory = (baseURL, stateMap, dispatch) => { methods[method], dispatch, actions, - cache + cache, + standardQuery ), ]); diff --git a/src/lib/stateToRedux.js b/src/lib/stateToRedux.js index d04d1f8..87e37a5 100644 --- a/src/lib/stateToRedux.js +++ b/src/lib/stateToRedux.js @@ -1,10 +1,14 @@ -import { camelCase, keys, snakeCase } from "lodash"; -import { crossProduct } from "./utils"; +import { keys, last, snakeCase } from "lodash"; +import { crossProduct, trie } from "./utils"; +import { listOfReducersToReducer } from "./ListToReducer"; +import { combineReducers } from "redux"; +import { connect as rconnect } from "react-redux"; /** * reducer factories. Simplifies the generation of reducers + * @typedef {(state, action) => action.payload} handler */ -const defHandlers = { +export const defaultHandlers = { set: (state, action) => { return action.payload; }, @@ -16,7 +20,39 @@ const defHandlers = { }, }; -const action = (type) => (payload) => ({ type, payload }); +// add new reducers here +// const reducerList = [fromStateMap(apiStateMap).reducers, { auth }, { alert }]; + +const getResourcePaths = (stateMap, prefix = []) => { + const nested = keys(stateMap).filter((k) => k.startsWith("_")); + const unnested = keys(stateMap).filter((k) => !k.startsWith("_")); + const level = unnested.map((resourceName) => [ + ...prefix, + resourceName, + stateMap[resourceName], + ]); + if (nested.length === 0) return level; + + return [ + ...level, + ...nested + .map((nameSpace) => + getResourcePaths(stateMap[nameSpace], [...prefix, nameSpace.slice(1)]) + ) + .flat(), + ]; +}; + +export const objectify = (state) => + trie(keys(state).map((k) => [...k.split("/"), state[k]])).tree; + +export const connect = (mapStateToProps, actions) => + rconnect((state) => mapStateToProps(objectify(state)), actions); + +const getResourceName = (resourcePath) => + resourcePath.slice(0, resourcePath.length - 1).join("/"); + +const action = (dispatch, type) => (payload) => dispatch({ type, payload }); const getVerb = (actionType) => actionType.split("_")[0].toLowerCase(); const getResource = (actionType) => actionType.split("_")[1]?.toLowerCase(); /** @@ -34,30 +70,50 @@ const getResource = (actionType) => actionType.split("_")[1]?.toLowerCase(); * and reducers for types "SET_CAT", and "UPDATE_CAT" * */ -export const fromStateMap = (stateMap, handlers = defHandlers) => { - const resourceNames = keys(stateMap); +/** @type { + + (stateMap: T, handlers: S ) => + {actions: + {[k in K]: { + [s in V]: (payload:{})=>{type:[s, k], payload:{}} + } + } + } + } +*/ +export const generateActions = (dispatch, stateMap, _handlers = {}) => { + const handlers = { ...defaultHandlers, _handlers }; + const resourcePaths = getResourcePaths(stateMap); const verbs = keys(handlers); - return { - actions: makeActions(verbs, resourceNames), - reducers: makeReducers(stateMap, resourceNames, handlers), - }; + return makeActions(verbs, resourcePaths, dispatch); }; -function makeActions(verbs, resourceNames) { - const actionList = crossProduct(verbs, resourceNames).map( - ([verb, resource]) => { +export const generateReducers = (stateMap, _handlers = {}) => { + const handlers = { ...defaultHandlers, _handlers }; + const resourcePaths = getResourcePaths(stateMap); + return combineReducers(makeReducers(stateMap, resourcePaths, handlers)); +}; + +function makeActions(verbs, resourcePaths, dispatch) { + const actionList = crossProduct(verbs, resourcePaths).map( + ([verb, resourcePath]) => { + const resource = getResourceName(resourcePath); const type = snakeCase(`${verb}_${resource}`).toUpperCase(); - return { [camelCase(type)]: action(type) }; + return [ + ...resourcePath.slice(0, resourcePath.length - 1), + verb, + action(dispatch, type), + ]; } ); - return Object.assign({}, ...actionList); + return trie(actionList).tree; } -function makeReducers(stateMap, resourceNames, handlers = defHandlers) { +function makeReducers(stateMap, resourcePaths, handlers) { // this returns a map from the keys in stateMap to a generic verb based reducer - const reducerList = resourceNames.map((resource) => { - const defaultState = stateMap[resource]; - + const reducerList = resourcePaths.map((resourcePath) => { + const defaultState = last(resourcePath); + const resource = getResourceName(resourcePath); const reducer = (state = defaultState, action) => { if (getResource(action.type) !== resource) return state; const handler = handlers[getVerb(action.type)]; @@ -67,5 +123,5 @@ function makeReducers(stateMap, resourceNames, handlers = defHandlers) { return { [resource]: reducer }; }); - return Object.assign({}, ...reducerList); + return listOfReducersToReducer([Object.assign({}, ...reducerList)]); } From d138947e6583a1b0a65667b63bb6d1cac40cbeec Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Sun, 7 Feb 2021 21:04:34 -0700 Subject: [PATCH 6/6] refactoring auth/login no tests run --- src/App.js | 15 +++++++++------ src/components/auth/Login.js | 19 ++++++++++++++----- src/store.js | 10 ++++------ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/App.js b/src/App.js index 42129a9..e2b9330 100644 --- a/src/App.js +++ b/src/App.js @@ -12,7 +12,7 @@ import Footer from "./components/Footer"; import ContactUs from "./components/ContactUs"; import FAQ from "./components/FAQ"; import Header from "./components/Header/Header"; -import { api } from "./store"; +import { actions, api } from "./store"; import classPage from "./components/classPage/classPage"; import ScrollToTop from "./components/ScrollToTop"; import Login from "./components/auth/Login"; @@ -20,7 +20,6 @@ import SignUp from "./components/auth/SignUp"; import BreadCrumb from "./components/Breadcrumbs/Breadcrumbs"; import Cat from "./components/Cat"; import { loadUser } from "./actions/auth"; -import { store } from "./store"; import setAuthToken from "./utils/setAuthToken"; import Dashboard from "./components/dashboard/Dashboard"; import { connect } from "./lib/stateToRedux"; @@ -31,13 +30,17 @@ if (localStorage.token) { } function App(props) { - useEffect(() => { - store.dispatch(loadUser()); - }, []); - + api.auth.get().then((data) => + actions.auth.auth.update({ + isAuthenicated: true, + loaded: false, + user: data, + }) + ); const { books, categories } = props; api.book.read(); api.category.read(); + api.user.get(); let cats = Object.values(categories); // covert categories to array const getBookTitle = (props) => { const id = props.match.params.id; diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js index eca3970..6a563d1 100644 --- a/src/components/auth/Login.js +++ b/src/components/auth/Login.js @@ -10,7 +10,6 @@ const Login = ({ isAuthenticated }) => { email: "", password: "", }); - const { email, password } = formData; const onChange = (e) => @@ -18,10 +17,20 @@ const Login = ({ isAuthenticated }) => { const onSubmit = async (e) => { e.preventDefault(); - api.auth.post({ email, password }).then(({ payload }) => { - localStorage.setItem("token", payload.token); - actions.auth.update({ token: payload.token }); - }); + api.auth + .post({ email, password }) + .then(({ payload }) => { + localStorage.setItem("token", payload.token); + actions.auth.update({ token: payload.token }); + }) + .catch(() => { + localStorage.removeItem("token"); + actions.auth.update({ + token: null, + isAuthenicated: false, + loading: false, + }); + }); }; //redirect if loggedin diff --git a/src/store.js b/src/store.js index d60ab5d..d635276 100644 --- a/src/store.js +++ b/src/store.js @@ -3,9 +3,6 @@ import { apiFactory } from "./lib/api"; import thunk from "redux-thunk"; import { applyMiddleware, compose, createStore } from "redux"; import promise from "redux-promise-middleware"; -import rootReducer from "./reducers"; -import auth, { initialState } from "./reducers/auth"; -import alert from "./reducers/alert"; // Add api endpoints here const _api = { @@ -14,15 +11,16 @@ const _api = { image: {}, user: {}, userhistory: {}, + auth: {}, +}; + +const _auth = { auth: { token: localStorage.getItem("token"), isAuthenticated: null, loading: true, user: null, }, -}; - -const _auth = { alerts: [], };