feat(backend): switch to toasts for client-side API feedback, flesh out edit profile page
This commit is contained in:
		
							parent
							
								
									8ab4c2a91b
								
							
						
					
					
						commit
						373ccf4b63
					
				
					 6 changed files with 77 additions and 29 deletions
				
			
		|  | @ -32,6 +32,7 @@ type EditableCardProps = { | |||
|   onChangeName: React.ChangeEventHandler<HTMLInputElement>; | ||||
|   onChangePronoun: React.ChangeEventHandler<HTMLInputElement>; | ||||
|   onAddPronoun(pronoun: string): void; | ||||
|   onDeletePronoun(e: React.MouseEvent<HTMLButtonElement>, entry: string): void; | ||||
|   onChangeFavourite( | ||||
|     e: React.MouseEvent<HTMLButtonElement>, | ||||
|     entry: string | ||||
|  | @ -125,6 +126,7 @@ export function EditableCard(props: EditableCardProps) { | |||
|                 </button> | ||||
|                 <button | ||||
|                   type="button" | ||||
|                   onClick={(e) => props.onDeletePronoun(e, pronoun)} | ||||
|                   className="bg-red-600 dark:bg-red-700 hover:bg-red-700 hover:dark:bg-red-800 p-2" | ||||
|                 > | ||||
|                   <Trash3 /> | ||||
|  | @ -138,6 +140,8 @@ export function EditableCard(props: EditableCardProps) { | |||
|           <Button | ||||
|             style={ButtonStyle.success} | ||||
|             onClick={() => { | ||||
|               if (!input || input === "") return; | ||||
| 
 | ||||
|               props.onAddPronoun(input); | ||||
|               setInput(""); | ||||
|             }} | ||||
|  |  | |||
							
								
								
									
										29
									
								
								frontend/lib/toast.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/lib/toast.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| import Toastify from "toastify-js"; | ||||
| import "toastify-js/src/toastify.css"; | ||||
| 
 | ||||
| export default function toast(options: { text: string; background?: string }) { | ||||
|   let background: string; | ||||
|   switch (options.background) { | ||||
|     case "error": | ||||
|       background = "#A1081F"; | ||||
|       break; | ||||
|     case "success": | ||||
|       background = "#1D611A"; | ||||
|       break; | ||||
|     default: | ||||
|       background = "#4F5859"; | ||||
|       break; | ||||
|   } | ||||
| 
 | ||||
|   Toastify({ | ||||
|     text: options.text, | ||||
|     gravity: "top", | ||||
|     position: "left", | ||||
|     duration: -1, | ||||
|     close: true, | ||||
|     style: { | ||||
|       background: background, | ||||
|       color: "#FFFFFF", | ||||
|     }, | ||||
|   }).showToast(); | ||||
| } | ||||
|  | @ -17,9 +17,9 @@ | |||
|     "react-dom": "18.2.0", | ||||
|     "react-markdown": "^8.0.3", | ||||
|     "react-sortablejs": "^6.1.4", | ||||
|     "react-toast": "^1.0.3", | ||||
|     "recoil": "^0.7.5", | ||||
|     "sortablejs": "^1.15.0" | ||||
|     "sortablejs": "^1.15.0", | ||||
|     "toastify-js": "^1.12.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tailwindcss/forms": "^0.5.2", | ||||
|  | @ -28,6 +28,7 @@ | |||
|     "@types/react": "18.0.15", | ||||
|     "@types/react-dom": "18.0.6", | ||||
|     "@types/sortablejs": "^1.13.0", | ||||
|     "@types/toastify-js": "^1.11.1", | ||||
|     "autoprefixer": "^10.4.7", | ||||
|     "eslint": "8.19.0", | ||||
|     "eslint-config-next": "13.0.4", | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { useRouter } from "next/router"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useRecoilState, useRecoilValue } from "recoil"; | ||||
| import { useRecoilState } from "recoil"; | ||||
| import Loading from "../../components/Loading"; | ||||
| import fetchAPI from "../../lib/fetch"; | ||||
| import { userState } from "../../lib/state"; | ||||
|  | @ -16,9 +16,10 @@ import { | |||
| 
 | ||||
| import Button, { ButtonStyle } from "../../components/Button"; | ||||
| import { Plus, Save, Trash } from "react-bootstrap-icons"; | ||||
| import toast from "../../lib/toast"; | ||||
| 
 | ||||
| export default function Index() { | ||||
|   const user = useRecoilValue(userState); | ||||
|   const [user, setUser] = useRecoilState(userState); | ||||
|   const router = useRouter(); | ||||
|   const [state, setState] = useState(cloneDeep(user)); | ||||
| 
 | ||||
|  | @ -82,20 +83,21 @@ export default function Index() { | |||
|         {isEdited && ( | ||||
|           <Button | ||||
|             style={ButtonStyle.success} | ||||
|             onClick={() => | ||||
|               updateUser({ | ||||
|             onClick={async () => { | ||||
|               const user = await updateUser({ | ||||
|                 displayName: state!.display_name, | ||||
|                 bio: state!.bio, | ||||
|                 fields, | ||||
|               }) | ||||
|             } | ||||
|               }); | ||||
| 
 | ||||
|               if (user) setUser(user); | ||||
|             }} | ||||
|           > | ||||
|             <Save aria-hidden className="inline" /> Save changes | ||||
|           </Button> | ||||
|         )} | ||||
|       </h1> | ||||
| 
 | ||||
|       <div>{`fieldsUpdated: ${fieldsUpdated}`}</div> | ||||
|       <h3 className="p-2 border-b border-slate-300 dark:border-slate-600 flex items-center justify-between"> | ||||
|         <span className="text-xl">Fields</span> | ||||
|         <div className="inline"> | ||||
|  | @ -146,6 +148,10 @@ export default function Index() { | |||
|               field.pronouns[pronoun] = PronounChoice.okay; | ||||
|               setFields([...fields]); | ||||
|             }} | ||||
|             onDeletePronoun={(e, pronoun) => { | ||||
|               delete field.pronouns[pronoun]; | ||||
|               setFields([...fields]); | ||||
|             }} | ||||
|             onChangeName={(e) => { | ||||
|               field.name = e.target.value; | ||||
|               setFields([...fields]); | ||||
|  | @ -232,9 +238,17 @@ async function updateUser(args: { | |||
|     return field; | ||||
|   }); | ||||
| 
 | ||||
|   return await fetchAPI<MeUser>("/users/@me", "PATCH", { | ||||
|     display_name: args.displayName ?? null, | ||||
|     bio: args.bio ?? null, | ||||
|     fields: newFields, | ||||
|   }); | ||||
|   try { | ||||
|     const user = await fetchAPI<MeUser>("/users/@me", "PATCH", { | ||||
|       display_name: args.displayName ?? null, | ||||
|       bio: args.bio ?? null, | ||||
|       fields: newFields, | ||||
|     }); | ||||
| 
 | ||||
|     toast({ text: "Successfully updated your profile!" }); | ||||
| 
 | ||||
|     return user; | ||||
|   } catch (e: any) { | ||||
|     toast({ text: `${e.message ?? e}`, background: "error" }); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -3,12 +3,13 @@ import { useRouter } from "next/router"; | |||
| import { useRecoilState } from "recoil"; | ||||
| import fetchAPI from "../../lib/fetch"; | ||||
| import { userState } from "../../lib/state"; | ||||
| import { MeUser, SignupResponse } from "../../lib/types"; | ||||
| import { APIError, MeUser, SignupResponse } from "../../lib/types"; | ||||
| import TextInput from "../../components/TextInput"; | ||||
| import Loading from "../../components/Loading"; | ||||
| import Button, { ButtonStyle } from "../../components/Button"; | ||||
| import Notice from "../../components/Notice"; | ||||
| import BlueLink from "../../components/BlueLink"; | ||||
| import toast from "../../lib/toast"; | ||||
| 
 | ||||
| interface CallbackResponse { | ||||
|   has_account: boolean; | ||||
|  | @ -126,21 +127,15 @@ export default function Discord() { | |||
|       setUser(resp.user); | ||||
|       localStorage.setItem("pronouns-token", resp.token); | ||||
| 
 | ||||
|       toast({ text: "Created account!", background: "success" }); | ||||
|       router.push("/"); | ||||
|     } catch (e) { | ||||
|       setState({ ...state, error: e }); | ||||
|     } catch (e: any) { | ||||
|       toast({ text: `${e.message ?? e}`, background: "error" }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       {state.error && ( | ||||
|         <Notice style={ButtonStyle.danger} header="Create account error"> | ||||
|           <p>{state.error.message ?? state.error}</p> | ||||
|           <p>Try again?</p> | ||||
|         </Notice> | ||||
|       )} | ||||
| 
 | ||||
|       <div className="border-slate-200 dark:border-slate-700 border rounded max-w-xl"> | ||||
|         <div className="border-b border-slate-200 dark:border-slate-700 p-2"> | ||||
|           <h1 className="font-bold text-xl">Get started</h1> | ||||
|  |  | |||
|  | @ -398,6 +398,11 @@ | |||
|   resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.13.0.tgz#870223438f8f2cd81157b128a4c0261adbcaa946" | ||||
|   integrity sha512-C3064MH72iEfeGCYEGCt7FCxXoAXaMPG0QPnstcxvPmbl54erpISu06d++FY37Smja64iWy5L8wOyHHBghWbJQ== | ||||
| 
 | ||||
| "@types/toastify-js@^1.11.1": | ||||
|   version "1.11.1" | ||||
|   resolved "https://registry.yarnpkg.com/@types/toastify-js/-/toastify-js-1.11.1.tgz#48f96596e087025c7f7821668599fd74dcdd8549" | ||||
|   integrity sha512-Ef03kGFWseAQYIQwN83WbhRxD+DOd+X6p22j9olA/TnvE0crDMc3fyoctKSpXgEDVWq5l3p98otIdpNX1pOYMA== | ||||
| 
 | ||||
| "@types/unist@*", "@types/unist@^2.0.0": | ||||
|   version "2.0.6" | ||||
|   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" | ||||
|  | @ -2650,11 +2655,6 @@ react-sortablejs@^6.1.4: | |||
|     classnames "2.3.1" | ||||
|     tiny-invariant "1.2.0" | ||||
| 
 | ||||
| react-toast@^1.0.3: | ||||
|   version "1.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/react-toast/-/react-toast-1.0.3.tgz#cbe2cd946c5762736642dd2981a7e5d666c5448e" | ||||
|   integrity sha512-gL3+O5hlLaoBmd36oXWKrjFeUyLCMQ04AIh48LrnUvdeg2vhJQ0E803TgVemgJvYUXKlutMVn9+/QS2DDnk26Q== | ||||
| 
 | ||||
| react@18.2.0: | ||||
|   version "18.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" | ||||
|  | @ -3078,6 +3078,11 @@ to-regex-range@^5.0.1: | |||
|   dependencies: | ||||
|     is-number "^7.0.0" | ||||
| 
 | ||||
| toastify-js@^1.12.0: | ||||
|   version "1.12.0" | ||||
|   resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.12.0.tgz#cc1c4f5c7e7380e854e20bedceb51980ea29f64d" | ||||
|   integrity sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ== | ||||
| 
 | ||||
| tr46@~0.0.3: | ||||
|   version "0.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue