diff --git a/client/package.json b/client/package.json index 6b97e4f..148c6da 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,7 @@ "dependencies": { "@tinymce/tinymce-react": "^4.2.0", "axios": "^1.2.0", + "jwt-decode": "^3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.4.3", @@ -18,6 +19,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@types/jwt-decode": "^3.1.0", "@types/react": "^18.0.24", "@types/react-dom": "^18.0.8", "@types/uuid": "^8.3.4", diff --git a/client/src/App.tsx b/client/src/App.tsx index 5e7c3a7..653e17f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,11 +1,11 @@ // framework tools and custom utils -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { AuthContext, IAuthContext, useAuthContext } from './context/AuthContext'; -import { attemptLogout, checkCredientials } from './util/apiUtils'; -import { IUser } from './schemas'; +import { useAuthContext } from './context/AuthContext'; +import jwtDecode from 'jwt-decode'; +import API from './util/API'; -// pages, ui, styles +// pages, ui, components, styles import Subscriptions from './components/pages/Subscriptions/Subscriptions'; import Browser from './components/ui/Browser'; import Collection from './components/pages/Collection'; @@ -19,55 +19,51 @@ import CollectionBrowser from './components/pages/CollectionBrowser'; import { Navbar } from './components/ui'; import GroceryList from './components/pages/GroceryList'; import GroceryListCollection from './components/pages/GroceryListCollection'; +import { TokenType } from './util/types'; import './sass/App.scss'; +import handleToken from './util/handleToken'; function App() { - const [user, setUser] = useState(); - const parentState = { user, setUser }; - - const receiveChange = (() => {}); + const { setUser, token, setToken } = useAuthContext(); useEffect(() => { - const wrapper = async () => { - try { - const result: IAuthContext | undefined = await checkCredientials(); - - if (result == undefined) { - setUser({ user: undefined }); - } else { - setUser(result); - } - } catch(e) { - console.error(e); + if (document.cookie) { + const response = handleToken(); + if (response) { + setToken(response.token); + setUser(response.user); } } + }, [document.cookie]); - wrapper(); - }, []) + useEffect(() => { + if (token) { + const response = handleToken(); + response && setUser(response.user); + } + }, [setToken]) return ( - -
- - - } /> - } /> - } /> - } /> - } /> - } /> - {}} />} /> - } /> - } /> - } /> +
+ + + } /> + } /> + } /> + } /> + } /> + } /> + {}} />} /> + } /> + } /> + } /> - } /> - } /> - } /> - -
- + } /> + } /> + } /> +
+
) } diff --git a/client/src/components/pages/AddRecipe.tsx b/client/src/components/pages/AddRecipe.tsx index 9cd599b..792e07e 100644 --- a/client/src/components/pages/AddRecipe.tsx +++ b/client/src/components/pages/AddRecipe.tsx @@ -10,7 +10,7 @@ const AddRecipe = () => { const getFormState = useCallback((data: IRecipe) => { setInput(data); - }, []) + }, [input]) const handleCreate = () => { for (let field of Object.keys(input)) { @@ -28,20 +28,6 @@ const AddRecipe = () => { } }) }, [authContext]) - - useEffect(() => { - input.authoruserid && setForm( - new Form({ - parent: "AddRecipe", - keys: ["name", "preptime", "course", "cuisine", "ingredients", "description"], - labels: ["Recipe Name:", "Prep Time:", "Course:", "Cuisine:", "Ingredients:", "Description:"], - dataTypes: ['text', 'text', 'custom picker', 'custom picker', 'custom picker', 'TINYMCE'], - initialState: input, - getState: getFormState, - richTextInitialValue: "

Enter recipe details here!

" - }).mount() - ) - }, [input.authoruserid]) useEffect(() => { console.log(input); @@ -53,7 +39,18 @@ const AddRecipe = () => { +
Enter recipe details here!

" + }} /> + { form ||

Loading...

} + diff --git a/client/src/components/pages/Login.tsx b/client/src/components/pages/Login.tsx index 8fd6a74..4ad816a 100644 --- a/client/src/components/pages/Login.tsx +++ b/client/src/components/pages/Login.tsx @@ -1,46 +1,40 @@ import { useCallback, useContext, useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; import { AuthContext, useAuthContext } from "../../context/AuthContext"; -import { attemptLogin } from "../../util/apiUtils"; -import { IUserAuth } from "../../schemas"; +import { useNavigate, useParams } from "react-router-dom"; +import { IUser, IUserAuth } from "../../schemas"; import { Button, Form, Page, Panel } from "../ui"; +import { FormConfig } from "../ui/Form"; +import API from "../../util/API"; export default function Login() { const params = new URLSearchParams(window.location.search); const redirect = params.get("redirect"); - const { user, setUser } = useContext(AuthContext); + const { user, setToken } = useContext(AuthContext); // setup and local state const navigate = useNavigate(); - const [form, setForm] = useState(); const [input, setInput] = useState({ email: '', password: '' }); // retrieve and store state from form const getFormState = useCallback((received: IUserAuth) => { setInput(received); - }, []) + }, [input]) const handleLogin = async () => { if (!input.email || !input.password) return; - const { data, ok } = await attemptLogin(input); - if (ok) setUser(data); + const result = await new API.Auth().login(input); + + // setting token will trigger ui update + setToken(result.token); + + // if there is a redirect, go there, else go home navigate(`/${redirect ?? ''}`); } // check for logged in user and mount form useEffect(() => { if (user) navigate('/'); - setForm( - new Form({ - parent: 'login', - keys: Object.keys(input), - labels: ["Email", "Password"], - dataTypes: Object.keys(input), - initialState: input, - getState: getFormState - }).mount() - ); }, []) return ( @@ -48,8 +42,18 @@ export default function Login() {

Hello! Nice to see you again.

- { form ||

Loading...

} + + + +
diff --git a/client/src/components/pages/Profile.tsx b/client/src/components/pages/Profile.tsx index 73077a6..d319d30 100644 --- a/client/src/components/pages/Profile.tsx +++ b/client/src/components/pages/Profile.tsx @@ -14,8 +14,7 @@ export default function Profile() { return (
-

{user?.firstname}'s Profile

-

Things and stuff!

+

{user && user.firstname}'s Profile

diff --git a/client/src/components/pages/Register/aboutyou.tsx b/client/src/components/pages/Register/aboutyou.tsx index 3b6ad87..05865ce 100644 --- a/client/src/components/pages/Register/aboutyou.tsx +++ b/client/src/components/pages/Register/aboutyou.tsx @@ -5,9 +5,10 @@ import { RegisterVariantType, VariantLabel } from "."; import { useAuthContext } from "../../../context/AuthContext"; import { IUser, IUserAuth } from "../../../schemas"; import { attemptLogin, attemptRegister } from "../../../util/apiUtils"; +import API from "../../../util/API"; import { Button, Page, Panel } from "../../ui"; import Divider from "../../ui/Divider"; -import Form, { FormConfig } from "../../ui/Form"; +import Form from "../../ui/Form"; const blankUser: IUser = { firstname: '', @@ -20,51 +21,31 @@ const blankUser: IUser = { } const AboutYou: RegisterVariantType = ({ transitionDisplay }) => { + const auth = new API.Auth(); const navigate = useNavigate(); - const authContext = useAuthContext(); - const [form, setForm] = useState(

Loading content...

); + const { user, setToken } = useAuthContext(); const [input, setInput] = useState(blankUser); - const [regSuccess, setRegSuccess] = useState(); const getFormState = useCallback((received: IUser) => { setInput(received); }, []); useEffect(() => { - if (authContext.user) navigate('/'); - }, [authContext]); + if (user) navigate('/'); + }, [user]); async function handleRegister() { - const res = await attemptRegister(input); + const res = await auth.register(input); if (res.ok) { + setTimeout(async () => { + const result = await auth.login(input); + setToken(result.token); + }, 750); + transitionDisplay(VariantLabel.InitialCollection, input); } } - async function unwrapLogin() { - const data: IUserAuth = { email: input.email, password: input.password || "" } - const login = await attemptLogin(data); - if (login) { - authContext.user = login.user; - } - navigate('/'); - } - - useEffect(() => { - setForm(new Form({ - parent: "register", - keys: ['firstname', 'lastname', 'handle', 'email', 'password'], - initialState: input, - labels: ['First Name', 'Last Name', 'Handle', 'Email', "Password"], - dataTypes: ['text', 'text', 'text', 'email', 'password'], - getState: getFormState - }).mount()); - }, []) - - useEffect(() => { - if (regSuccess) unwrapLogin(); - }, [regSuccess]) - return (

Hi! Thanks for being here.

@@ -74,7 +55,16 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {

Tell us a bit about yourself:

- { form ||

Loading...

} + + +
diff --git a/client/src/components/pages/Register/collection.tsx b/client/src/components/pages/Register/collection.tsx index ee97dfc..70ab9ea 100644 --- a/client/src/components/pages/Register/collection.tsx +++ b/client/src/components/pages/Register/collection.tsx @@ -3,33 +3,24 @@ import { RegisterVariantType, VariantLabel } from "."; import { useNow } from "../../../hooks/useNow"; import { ICollection, IUser, IUserAuth } from "../../../schemas"; import { attemptLogin, createNewCollection } from "../../../util/apiUtils"; +import API from "../../../util/API"; import { Button, Divider, Page, Panel } from "../../ui"; import TextField from "../../ui/TextField"; +import { useAuthContext } from "../../../context/AuthContext"; -const InitialCollection: RegisterVariantType = ({ transitionDisplay, receiveChange, input }) => { +const InitialCollection: RegisterVariantType = ({ transitionDisplay, input }) => { + const { user, token } = useAuthContext(); const [collectionName, setCollectionName] = useState(); const [view, setView] = useState(

Loading...

); - const [user, setUser] = useState(); const now = useNow(); - async function unwrapLogin(data: IUser) { - const userInfo: IUserAuth = { email: data.email, password: data.password! } - const login = await attemptLogin(userInfo); - setUser(login.user); - } - - useEffect(() => { - if (input) { - setTimeout(() => { - unwrapLogin(input); - }, 750); - } - }, []) - const handleClick = async () => { - if (!user) return; + if (!user || !token) return; + + const collectionAPI = new API.Collection(token); + const collection: ICollection = { - name: collectionName || (user.firstname + "'s Collection"), + name: collectionName ?? (user.firstname + "'s Collection"), active: true, ismaincollection: true, ownerid: user.id!.toString(), @@ -39,14 +30,13 @@ const InitialCollection: RegisterVariantType = ({ transitionDisplay, receiveChan console.log(collection); - const result = await createNewCollection(collection); + const result = await collectionAPI.post(collection); console.log(result); if (result) transitionDisplay(VariantLabel.AddFriends); } useEffect(() => { - if (user && receiveChange) { - receiveChange(user); + if (user && token) { setView(

Hi, {user.firstname}! Great to meet you.

@@ -59,7 +49,8 @@ const InitialCollection: RegisterVariantType = ({ transitionDisplay, receiveChan

What would you like to call your main collection?

- ) => setCollectionName(e.target.value)} placeholder={user.firstname + 's Collection'} /> + {/* ) => setCollectionName(e.target.value)} placeholder={user.firstname + 's Collection'} /> */} + setCollectionName(e.target.value)} placeholder={user.firstname + 's Collection'}> diff --git a/client/src/components/pages/Register/index.tsx b/client/src/components/pages/Register/index.tsx index 4d42422..bf39270 100644 --- a/client/src/components/pages/Register/index.tsx +++ b/client/src/components/pages/Register/index.tsx @@ -19,17 +19,17 @@ export enum VariantLabel { FinishUp } -const Register: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange }) => { +const Register = () => { const [displayed, setDisplayed] = useState(); - const authContext = useAuthContext(); + const { user } = useAuthContext(); - const transitionDisplay = (variant: number | VariantLabel, user?: IUser) => { + const transitionDisplay = (variant: number | VariantLabel) => { switch (variant) { case 0: setDisplayed(); break; case 1: - setDisplayed(); + setDisplayed(); break; case 2: setDisplayed(); @@ -38,7 +38,7 @@ const Register: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange setDisplayed(); break; default: - setDisplayed(); + setDisplayed(); break; } } diff --git a/client/src/components/ui/Form.tsx b/client/src/components/ui/Form.tsx index f0541a8..7704977 100644 --- a/client/src/components/ui/Form.tsx +++ b/client/src/components/ui/Form.tsx @@ -1,13 +1,6 @@ -import { ChangeEvent, FC } from "react"; -import { v4 } from 'uuid'; -import RichText from "./RichText"; - -/** - * For the generation of more complex form objects with - * larger stateful values; expects to receive an object of - * type T to a form which can mutate T with a state setter - * of type Dispatch> -**/ +import { ChangeEvent, FC, useEffect, useState } from "react" +import { v4 } from "uuid" +import RichText from "./RichText" export interface FormConfig { parent: string @@ -20,83 +13,95 @@ export interface FormConfig { extraStyles?: string } -export default class Form { - private parent: string; - private labels: string[]; - private keys: string[]; - private dataTypes: any[] - private state: T; - private getState: (received: T) => void - private richTextInitialValue?: string; - private extraStyles?: string +interface FormProps { + parent: any + _config: FormConfig +} - constructor(config: FormConfig){ - this.parent = config.parent; - this.keys = config.keys; - this.labels = config.labels || this.keys; - this.dataTypes = config.dataTypes || new Array(this.keys.length).fill('text'); - this.state = config.initialState; - this.getState = config.getState; - this.richTextInitialValue = config.richTextInitialValue; - this.extraStyles = config.extraStyles; +const Form: FC = ({ parent, _config }) => { + type T = typeof parent; + const { getState } = _config; - this.mount(); - } + const [config, setConfig] = useState>(); + const [state, setState] = useState(); + const [contents, setContents] = useState(); - update(e: ChangeEvent, idx: number) { - let newState = { - ...this.state, - [this.keys[idx]]: e.target['value' as keyof EventTarget] - } + // initial setup + useEffect(() => { + if (!config) setConfig({ + ..._config, + labels: _config.labels ?? _config.keys, + dataTypes: _config.dataTypes ?? new Array(_config.keys?.length).fill("text"), + }); - this.state = newState; - this.getState(newState); - } + if (!state) setState(_config.initialState); + }, []) - updateRichText(txt: string, idx: number) { - this.state = { - ...this.state, - [this.keys[idx]]: txt - } + // usecallback handling + useEffect(() => { + state && getState(state); + }, [state]); - this.getState(this.state); - } + // update methods + function updateRichText(txt: string, idx: number) { + if (!config) return; - mount() { - let output = new Array(); - - for (let i = 0; i < this.keys.length; i++) { - let input: JSX.Element | null; - - if (this.dataTypes[i] == 'custom picker') { - console.log('noted!'); - this.dataTypes[i] = 'text'; + setState((prev: T) => { + return { + ...prev, + [config.keys[idx]]: txt } - - if (this.dataTypes[i] == 'TINYMCE') { - input = ( -
- - this.updateRichText(txt, i)} /> -
- ) - } else { - input = ( -
- - this.update(e, i)} - value={this.state[i as keyof T] as string}> - -
- ) - } - - output.push(input); - } - - return
{output}
; + }) } -} \ No newline at end of file + + function update(e: ChangeEvent, idx: number) { + if (!config) return; + + setState((prev: T) => { + return { + ...prev, + [config.keys[idx]]: e.target['value' as keyof EventTarget] + } + }) + } + + // mount the form once config has been loaded + useEffect(() => { + if (state && config) { + const result = config.keys.map((each: string, i: number) => { + + if (config.dataTypes![i] == 'TINYMCE') { + return ( +
+ + updateRichText(txt, i)} /> +
+ ) + } else { + return ( +
+ + update(e, i)} + value={state[i as keyof T] as string}> + +
+ ) + } + }); + + setContents(result); + + } + }, [config]); + + return ( +
+ { contents } +
+ ) +} + +export default Form; \ No newline at end of file diff --git a/client/src/components/ui/Navbar/index.tsx b/client/src/components/ui/Navbar/index.tsx index b15b1db..8e3aca1 100644 --- a/client/src/components/ui/Navbar/index.tsx +++ b/client/src/components/ui/Navbar/index.tsx @@ -1,44 +1,25 @@ -import { FC, useCallback, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; import { LoggedIn, NotLoggedIn, Registering } from "./variants"; import { useAuthContext } from "../../../context/AuthContext"; -import { IUser } from "../../../schemas"; import "/src/sass/components/Navbar.scss"; -const Navbar: FC<{receiveChange: (change: IUser) => void}> = ({ receiveChange }) => { +const Navbar = () => { // setup and local state - const navigate = useNavigate(); - const { user, setUser } = useAuthContext(); - const [received, setReceived] = useState(); - const [displayed, setDisplayed] = useState(); - - // lift and store state from navbar variants - const liftChange = useCallback((newValue: IUser | undefined) => { - if (!newValue) { - return; - } - - setUser(newValue); - setReceived(newValue); - }, []) + const { user } = useAuthContext(); + const [displayed, setDisplayed] = useState(

Loading...

); const variants = { - loggedin: , - notloggedin: , - registering: + loggedin: , + notloggedin: , + registering: } // side effects for live rendering useEffect(() => { - user && setReceived(user); - }, [user]) + setDisplayed(user ? variants.loggedin : variants.notloggedin); + }, [user]); - useEffect(() => { - if (received) receiveChange(received); - setDisplayed(received ? variants.loggedin : variants.notloggedin); - }, [received, setReceived]); - - return displayed ||

Loading...

; + return displayed; } export default Navbar; \ No newline at end of file diff --git a/client/src/components/ui/Navbar/variants.tsx b/client/src/components/ui/Navbar/variants.tsx index a9fa831..923582d 100644 --- a/client/src/components/ui/Navbar/variants.tsx +++ b/client/src/components/ui/Navbar/variants.tsx @@ -1,15 +1,26 @@ -import { attemptLogout } from "../../../util/apiUtils"; +import API from "../../../util/API"; import { NavbarType } from "../../../util/types"; -import { Button, Dropdown } from '../.' -import { useState } from "react"; +import { Button, Dropdown } from '..' +import { useEffect, useState } from "react"; +import { useAuthContext } from "../../../context/AuthContext"; +import { useNavigate } from "react-router-dom"; + +const LoggedIn = () => { + const { user, setUser, setToken } = useAuthContext(); + const navigate = useNavigate(); + const auth = new API.Auth(); -const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => { const [dropdownActive, setDropdownActive] = useState(false); const [searchActive, setSearchActive] = useState(false); const handleLogout = async () => { - const success = await attemptLogout(); - if (success) liftChange!(undefined); + const success = await auth.logout(); + console.log(success); + + // nullify cookie and unset user/token data + document.cookie = `token=;expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + setUser(undefined); + setToken(undefined); navigate('/'); } @@ -29,6 +40,10 @@ const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => { navigate(payload); } + useEffect(() => { + console.log(user); + }, []) + return (
-

Hi, {received?.firstname}.

+

Hi, {user && user.firstname}.

@@ -64,7 +79,9 @@ const LoggedIn: NavbarType = ({ received, liftChange, navigate }) => { ) } -const NotLoggedIn: NavbarType = ({ navigate }) => { +const NotLoggedIn = () => { + const navigate = useNavigate(); + return (