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> |       <Routes> | ||||||
|         <Route index element={<Home />} /> |         <Route index element={<Home />} /> | ||||||
|         <Route path="/user/auth" element={<Auth />} /> |         <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" element={<WorkspaceDetail />} /> | ||||||
|         <Route path="/:workspaceName/go/:shortcutName" element={<ShortcutRedirector />} /> |         <Route path="/:workspaceName/go/:shortcutName" element={<ShortcutRedirector />} /> | ||||||
|       </Routes> |       </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; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     requestState.setLoading(); | ||||||
|     try { |     try { | ||||||
|       if (workspaceId) { |       if (workspaceId) { | ||||||
|         await workspaceService.patchWorkspace({ |         await workspaceService.patchWorkspace({ | ||||||
| @@ -79,6 +80,7 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => { | |||||||
|       console.error(error); |       console.error(error); | ||||||
|       toastHelper.error(error.response.data.error || error.response.data.message); |       toastHelper.error(error.response.data.error || error.response.data.message); | ||||||
|     } |     } | ||||||
|  |     requestState.setFinish(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
| @@ -110,6 +112,7 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => { | |||||||
|         </div> |         </div> | ||||||
|         <div className="w-full flex flex-row justify-end items-center"> |         <div className="w-full flex flex-row justify-end items-center"> | ||||||
|           <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" : ""}`} |             className={`rounded px-3 py-2 shadow bg-green-600 text-white hover:bg-green-700 ${requestState.isLoading ? "opacity-80" : ""}`} | ||||||
|             onClick={handleSaveBtnClick} |             onClick={handleSaveBtnClick} | ||||||
|           > |           > | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import store from "../../store"; | |||||||
| import "../../less/base-dialog.less"; | import "../../less/base-dialog.less"; | ||||||
|  |  | ||||||
| interface DialogConfig { | interface DialogConfig { | ||||||
|   className: string; |   className?: string; | ||||||
|   clickSpaceDestroy?: boolean; |   clickSpaceDestroy?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ const Header: React.FC = () => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="w-full bg-amber-50"> |     <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"> |         <div className="flex flex-row justify-start items-center"> | ||||||
|           <Link to={"/"} className="text-base font-mono font-medium cursor-pointer"> |           <Link to={"/"} className="text-base font-mono font-medium cursor-pointer"> | ||||||
|             Corgi |             Corgi | ||||||
| @@ -77,7 +77,7 @@ const Header: React.FC = () => { | |||||||
|               actions={ |               actions={ | ||||||
|                 <> |                 <> | ||||||
|                   <Link |                   <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" |                     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 |                     <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 { useAppSelector } from "../store"; | ||||||
| import Header from "../components/Header"; | 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 UserDetail: React.FC = () => { | ||||||
|   const { user } = useAppSelector((state) => state.user); |   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 ( |   return ( | ||||||
|     <div className="w-full h-full flex flex-col justify-start items-start"> |     <div className="w-full h-full flex flex-col justify-start items-start"> | ||||||
|       <Header /> |       <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="text-3xl mt-2 mb-4">{user?.name}</p> | ||||||
|         <p className="leading-10">Email: {user?.email}</p> |         <p className="leading-10 flex flex-row justify-start items-center"> | ||||||
|         <p className="leading-10">OpenID: {user?.openId}</p> |           <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> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { Link, useParams } from "react-router-dom"; | import { useParams } from "react-router-dom"; | ||||||
| import { shortcutService, workspaceService } from "../services"; | import { shortcutService, workspaceService } from "../services"; | ||||||
| import { useAppSelector } from "../store"; | import { useAppSelector } from "../store"; | ||||||
| import useLoading from "../hooks/useLoading"; | import useLoading from "../hooks/useLoading"; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Steven
					Steven