mirror of
				https://github.com/aykhans/slash-e.git
				synced 2025-10-31 08:59:59 +00:00 
			
		
		
		
	chore: update account page
This commit is contained in:
		| @@ -48,7 +48,7 @@ function App() { | ||||
|       <Routes> | ||||
|         <Route index element={<Home />} /> | ||||
|         <Route path="/user/auth" element={<Auth />} /> | ||||
|         <Route path="/user/:userId" element={<UserDetail />} /> | ||||
|         <Route path="/account/" element={<UserDetail />} /> | ||||
|         <Route path="/:workspaceName" element={<WorkspaceDetail />} /> | ||||
|         <Route path="/:workspaceName/go/:shortcutName" element={<ShortcutRedirector />} /> | ||||
|       </Routes> | ||||
|   | ||||
							
								
								
									
										123
									
								
								web/src/components/ChangePasswordDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								web/src/components/ChangePasswordDialog.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| import { useState } from "react"; | ||||
| import { validate, ValidatorConfig } from "../helpers/validator"; | ||||
| import useLoading from "../hooks/useLoading"; | ||||
| import { userService } from "../services"; | ||||
| import Icon from "./Icon"; | ||||
| import { generateDialog } from "./Dialog"; | ||||
| import toastHelper from "./Toast"; | ||||
|  | ||||
| const validateConfig: ValidatorConfig = { | ||||
|   minLength: 3, | ||||
|   maxLength: 24, | ||||
|   noSpace: true, | ||||
|   noChinese: true, | ||||
| }; | ||||
|  | ||||
| type Props = DialogProps; | ||||
|  | ||||
| const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => { | ||||
|   const [newPassword, setNewPassword] = useState(""); | ||||
|   const [newPasswordAgain, setNewPasswordAgain] = useState(""); | ||||
|   const requestState = useLoading(false); | ||||
|  | ||||
|   const handleCloseBtnClick = () => { | ||||
|     destroy(); | ||||
|   }; | ||||
|  | ||||
|   const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     const text = e.target.value as string; | ||||
|     setNewPassword(text); | ||||
|   }; | ||||
|  | ||||
|   const handleNewPasswordAgainChanged = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     const text = e.target.value as string; | ||||
|     setNewPasswordAgain(text); | ||||
|   }; | ||||
|  | ||||
|   const handleSaveBtnClick = async () => { | ||||
|     if (newPassword === "" || newPasswordAgain === "") { | ||||
|       toastHelper.error("Please fill all inputs"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (newPassword !== newPasswordAgain) { | ||||
|       toastHelper.error("Not matched"); | ||||
|       setNewPasswordAgain(""); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const passwordValidResult = validate(newPassword, validateConfig); | ||||
|     if (!passwordValidResult.result) { | ||||
|       toastHelper.error("New password is invalid"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     requestState.setLoading(); | ||||
|     try { | ||||
|       const user = userService.getState().user as User; | ||||
|       await userService.patchUser({ | ||||
|         id: user.id, | ||||
|         password: newPassword, | ||||
|       }); | ||||
|       toastHelper.info("Password changed"); | ||||
|       handleCloseBtnClick(); | ||||
|     } catch (error: any) { | ||||
|       console.error(error); | ||||
|       toastHelper.error(error.response.data.message); | ||||
|     } | ||||
|     requestState.setFinish(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="max-w-full w-80 flex flex-row justify-between items-center mb-4"> | ||||
|         <p className="text-base">Change Password</p> | ||||
|         <button className="rounded p-1 hover:bg-gray-100" onClick={destroy}> | ||||
|           <Icon.X className="w-5 h-auto text-gray-600" /> | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="w-full flex flex-col justify-start items-start"> | ||||
|         <div className="w-full flex flex-col justify-start items-start mb-3"> | ||||
|           <span className="mb-2">New Password</span> | ||||
|           <input | ||||
|             className="w-full rounded border text-sm shadow-inner px-2 py-2" | ||||
|             type="text" | ||||
|             value={newPassword} | ||||
|             onChange={handleNewPasswordChanged} | ||||
|           /> | ||||
|         </div> | ||||
|         <div className="w-full flex flex-col justify-start items-start mb-3"> | ||||
|           <span className="mb-2">New Password Again</span> | ||||
|           <input | ||||
|             className="w-full rounded border text-sm shadow-inner px-2 py-2" | ||||
|             type="text" | ||||
|             value={newPasswordAgain} | ||||
|             onChange={handleNewPasswordAgainChanged} | ||||
|           /> | ||||
|         </div> | ||||
|         <div className="w-full flex flex-row justify-end items-center"> | ||||
|           <button | ||||
|             disabled={requestState.isLoading} | ||||
|             className={`rounded px-3 py-2 ${requestState.isLoading ? "opacity-80" : ""}`} | ||||
|             onClick={destroy} | ||||
|           > | ||||
|             Cancel | ||||
|           </button> | ||||
|           <button | ||||
|             disabled={requestState.isLoading} | ||||
|             className={`rounded px-3 py-2 shadow bg-green-600 text-white hover:bg-green-700 ${requestState.isLoading ? "opacity-80" : ""}`} | ||||
|             onClick={handleSaveBtnClick} | ||||
|           > | ||||
|             Save | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| function showChangePasswordDialog() { | ||||
|   generateDialog({}, ChangePasswordDialog); | ||||
| } | ||||
|  | ||||
| export default showChangePasswordDialog; | ||||
| @@ -63,6 +63,7 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     requestState.setLoading(); | ||||
|     try { | ||||
|       if (workspaceId) { | ||||
|         await workspaceService.patchWorkspace({ | ||||
| @@ -79,6 +80,7 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => { | ||||
|       console.error(error); | ||||
|       toastHelper.error(error.response.data.error || error.response.data.message); | ||||
|     } | ||||
|     requestState.setFinish(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
| @@ -110,6 +112,7 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => { | ||||
|         </div> | ||||
|         <div className="w-full flex flex-row justify-end items-center"> | ||||
|           <button | ||||
|             disabled={requestState.isLoading} | ||||
|             className={`rounded px-3 py-2 shadow bg-green-600 text-white hover:bg-green-700 ${requestState.isLoading ? "opacity-80" : ""}`} | ||||
|             onClick={handleSaveBtnClick} | ||||
|           > | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import store from "../../store"; | ||||
| import "../../less/base-dialog.less"; | ||||
|  | ||||
| interface DialogConfig { | ||||
|   className: string; | ||||
|   className?: string; | ||||
|   clickSpaceDestroy?: boolean; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ const Header: React.FC = () => { | ||||
|  | ||||
|   return ( | ||||
|     <div className="w-full bg-amber-50"> | ||||
|       <div className="w-full max-w-4xl mx-auto px-3 py-4 flex flex-row justify-between items-center"> | ||||
|       <div className="w-full max-w-4xl mx-auto px-3 py-5 flex flex-row justify-between items-center"> | ||||
|         <div className="flex flex-row justify-start items-center"> | ||||
|           <Link to={"/"} className="text-base font-mono font-medium cursor-pointer"> | ||||
|             Corgi | ||||
| @@ -77,7 +77,7 @@ const Header: React.FC = () => { | ||||
|               actions={ | ||||
|                 <> | ||||
|                   <Link | ||||
|                     to={`/user/${user?.id}`} | ||||
|                     to="/account" | ||||
|                     className="w-full flex flex-row justify-start items-center px-3 leading-10 text-left cursor-pointer rounded whitespace-nowrap hover:bg-gray-100" | ||||
|                   > | ||||
|                     <Icon.User className="w-4 h-auto mr-1" /> My Account | ||||
|   | ||||
							
								
								
									
										25
									
								
								web/src/less/common-dialog.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								web/src/less/common-dialog.less
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| .common-dialog { | ||||
|   > .dialog-container { | ||||
|     @apply w-80; | ||||
|  | ||||
|     > .dialog-content-container { | ||||
|       @apply flex flex-col justify-start items-start; | ||||
|  | ||||
|       > .btns-container { | ||||
|         @apply flex flex-row justify-end items-center w-full mt-4; | ||||
|  | ||||
|         > .btn { | ||||
|           @apply text-sm py-1 px-3 mr-2 rounded-md cursor-pointer hover:opacity-80; | ||||
|  | ||||
|           &.confirm-btn { | ||||
|             @apply bg-red-100 border border-solid border-blue-600 text-blue-600; | ||||
|  | ||||
|             &.warning { | ||||
|               @apply border-red-600 text-red-600; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,16 +1,71 @@ | ||||
| import { useAppSelector } from "../store"; | ||||
| import Header from "../components/Header"; | ||||
| import { showCommonDialog } from "../components/Dialog/CommonDialog"; | ||||
| import { userService } from "../services"; | ||||
| import Icon from "../components/Icon"; | ||||
| import copy from "copy-to-clipboard"; | ||||
| import toastHelper from "../components/Toast"; | ||||
| import showChangePasswordDialog from "../components/ChangePasswordDialog"; | ||||
|  | ||||
| const UserDetail: React.FC = () => { | ||||
|   const { user } = useAppSelector((state) => state.user); | ||||
|  | ||||
|   const handleChangePasswordBtnClick = async () => { | ||||
|     showChangePasswordDialog(); | ||||
|   }; | ||||
|  | ||||
|   const handleCopyOpenIdBtnClick = async () => { | ||||
|     if (!user?.openId) { | ||||
|       toastHelper.error("OpenID not found"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     copy(user.openId); | ||||
|     toastHelper.success("OpenID copied"); | ||||
|   }; | ||||
|  | ||||
|   const handleResetOpenIdBtnClick = async () => { | ||||
|     showCommonDialog({ | ||||
|       title: "Reset Open API", | ||||
|       content: "❗️The existing API will be invalidated and a new one will be generated, are you sure you want to reset?", | ||||
|       style: "warning", | ||||
|       onConfirm: async () => { | ||||
|         await userService.patchUser({ | ||||
|           id: user?.id as UserId, | ||||
|           resetOpenID: true, | ||||
|         }); | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className="w-full h-full flex flex-col justify-start items-start"> | ||||
|       <Header /> | ||||
|       <div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start"> | ||||
|       <div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-3"> | ||||
|         <p className="text-3xl mt-2 mb-4">{user?.name}</p> | ||||
|         <p className="leading-10">Email: {user?.email}</p> | ||||
|         <p className="leading-10">OpenID: {user?.openId}</p> | ||||
|         <p className="leading-10 flex flex-row justify-start items-center"> | ||||
|           <span className="mr-3 text-gray-500 font-mono">Email: </span> | ||||
|           {user?.email} | ||||
|         </p> | ||||
|         <p className="leading-10 flex flex-row justify-start items-center"> | ||||
|           <span className="mr-3 text-gray-500 font-mono">Password: </span> | ||||
|           <button className="border rounded-md px-3 hover:shadow" onClick={handleChangePasswordBtnClick}> | ||||
|             Change | ||||
|           </button> | ||||
|         </p> | ||||
|         <p className="leading-10 flex flex-row justify-start items-center"> | ||||
|           <span className="mr-3 text-gray-500 font-mono">OpenID:</span> | ||||
|           <input type="text" value={user?.openId} readOnly className="border rounded-md px-3 pr-5 shadow-inner truncate" /> | ||||
|           <button className="-ml-6 bg-white text-gray-600 hover:text-black" onClick={handleCopyOpenIdBtnClick}> | ||||
|             <Icon.Clipboard className="w-4 h-auto" /> | ||||
|           </button> | ||||
|           <button | ||||
|             className="border ml-4 rounded-md px-3 border-red-600 text-red-600 bg-red-50 hover:shadow" | ||||
|             onClick={handleResetOpenIdBtnClick} | ||||
|           > | ||||
|             Reset | ||||
|           </button> | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { useEffect, useState } from "react"; | ||||
| import { Link, useParams } from "react-router-dom"; | ||||
| import { useParams } from "react-router-dom"; | ||||
| import { shortcutService, workspaceService } from "../services"; | ||||
| import { useAppSelector } from "../store"; | ||||
| import useLoading from "../hooks/useLoading"; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Steven
					Steven