From 9b303da4eba332c5e02ade8acc8c264989218a7b Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 23 Jun 2023 20:52:00 +0800 Subject: [PATCH] feat: implement edit userinfo --- api/v1/user.go | 19 +++-- web/src/components/ChangePasswordDialog.tsx | 14 ---- web/src/components/EditUserinfoDialog.tsx | 89 +++++++++++++++++++++ web/src/components/Header.tsx | 60 ++++++-------- web/src/helpers/validator.ts | 52 ------------ web/src/pages/Account.tsx | 37 +++++++++ web/src/pages/Auth.tsx | 5 +- web/src/pages/Home.tsx | 9 +-- web/src/pages/UserDetail.tsx | 63 --------------- web/src/routers/index.tsx | 10 +-- web/src/types/modules/user.d.ts | 4 +- 11 files changed, 173 insertions(+), 189 deletions(-) create mode 100644 web/src/components/EditUserinfoDialog.tsx delete mode 100644 web/src/helpers/validator.ts create mode 100644 web/src/pages/Account.tsx delete mode 100644 web/src/pages/UserDetail.tsx diff --git a/api/v1/user.go b/api/v1/user.go index eda6fe8..040563e 100644 --- a/api/v1/user.go +++ b/api/v1/user.go @@ -69,10 +69,10 @@ func (create CreateUserRequest) Validate() error { } type PatchUserRequest struct { - RowStatus *RowStatus `json:"rowStatus"` - Email *string `json:"email"` - DisplayName *string `json:"displayName"` - Password *string `json:"password"` + RowStatus *RowStatus `json:"rowStatus"` + Email *string `json:"email"` + Nickname *string `json:"nickname"` + Password *string `json:"password"` } type UserDelete struct { @@ -151,11 +151,16 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { updateUser := &store.UpdateUser{ ID: currentUserID, } + if userPatch.Email != nil { + if !validateEmail(*userPatch.Email) { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format") + } - if userPatch.Email != nil && !validateEmail(*userPatch.Email) { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format") + updateUser.Email = userPatch.Email + } + if userPatch.Nickname != nil { + updateUser.Nickname = userPatch.Nickname } - if userPatch.Password != nil && *userPatch.Password != "" { passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost) if err != nil { diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index 6fac33e..4ef68f4 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -1,18 +1,10 @@ import { Button, Input, Modal, ModalDialog } from "@mui/joy"; import { useState } from "react"; import { toast } from "react-hot-toast"; -import { validate, ValidatorConfig } from "../helpers/validator"; import useLoading from "../hooks/useLoading"; import { userService } from "../services"; import Icon from "./Icon"; -const validateConfig: ValidatorConfig = { - minLength: 3, - maxLength: 24, - noSpace: true, - noChinese: true, -}; - interface Props { onClose: () => void; } @@ -49,12 +41,6 @@ const ChangePasswordDialog: React.FC = (props: Props) => { return; } - const passwordValidResult = validate(newPassword, validateConfig); - if (!passwordValidResult.result) { - toast.error("New password is invalid"); - return; - } - requestState.setLoading(); try { const user = userService.getState().user as User; diff --git a/web/src/components/EditUserinfoDialog.tsx b/web/src/components/EditUserinfoDialog.tsx new file mode 100644 index 0000000..0458ce4 --- /dev/null +++ b/web/src/components/EditUserinfoDialog.tsx @@ -0,0 +1,89 @@ +import { Button, Input, Modal, ModalDialog } from "@mui/joy"; +import { useState } from "react"; +import { toast } from "react-hot-toast"; +import useLoading from "../hooks/useLoading"; +import { userService } from "../services"; +import Icon from "./Icon"; +import { useAppSelector } from "../stores"; + +interface Props { + onClose: () => void; +} + +const EditUserinfoDialog: React.FC = (props: Props) => { + const { onClose } = props; + const user = useAppSelector((state) => state.user.user as User); + const [email, setEmail] = useState(user.email); + const [nickname, setNickname] = useState(user.nickname); + const requestState = useLoading(false); + + const handleCloseBtnClick = () => { + onClose(); + }; + + const handleEmailChanged = (e: React.ChangeEvent) => { + const text = e.target.value as string; + setEmail(text); + }; + + const handleNicknameChanged = (e: React.ChangeEvent) => { + const text = e.target.value as string; + setNickname(text); + }; + + const handleSaveBtnClick = async () => { + if (email === "" || nickname === "") { + toast.error("Please fill all fields"); + return; + } + + requestState.setLoading(); + try { + const user = userService.getState().user as User; + await userService.patchUser({ + id: user.id, + email, + nickname, + }); + onClose(); + toast("Password changed"); + } catch (error: any) { + console.error(error); + toast.error(JSON.stringify(error.response.data)); + } + requestState.setFinish(); + }; + + return ( + + +
+ Edit Userinfo + +
+
+
+ Email + +
+
+ Nickname + +
+
+ + +
+
+
+
+ ); +}; + +export default EditUserinfoDialog; diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 50d0044..c7380c3 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,16 +1,14 @@ import { Link, useNavigate } from "react-router-dom"; import { useAppSelector } from "../stores"; -import { userService } from "../services"; import Icon from "./Icon"; import Dropdown from "./common/Dropdown"; const Header: React.FC = () => { const navigate = useNavigate(); - const { user } = useAppSelector((state) => state.user); + const user = useAppSelector((state) => state.user).user as User; const handleSignOutButtonClick = async () => { - await userService.doSignOut(); - navigate("/user/auth"); + navigate("/auth"); }; return ( @@ -24,37 +22,31 @@ const Header: React.FC = () => {
- {user ? ( - - {user.nickname} - + + {user.nickname} + + + } + actions={ + <> + + My Account + + - } - actions={ - <> - - My Account - - - - } - actionsClassName="!w-40" - > - ) : ( - navigate("/user/auth")}> - Sign in - - )} + + } + actionsClassName="!w-40" + >
diff --git a/web/src/helpers/validator.ts b/web/src/helpers/validator.ts deleted file mode 100644 index d02ceab..0000000 --- a/web/src/helpers/validator.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Validator -// * use for validating form data -const chineseReg = /[\u3000\u3400-\u4DBF\u4E00-\u9FFF]/; - -export interface ValidatorConfig { - // min length - minLength: number; - // max length - maxLength: number; - // no space - noSpace: boolean; - // no chinese - noChinese: boolean; -} - -export function validate(text: string, config: Partial): { result: boolean; reason?: string } { - if (config.minLength !== undefined) { - if (text.length < config.minLength) { - return { - result: false, - reason: "Too short", - }; - } - } - - if (config.maxLength !== undefined) { - if (text.length > config.maxLength) { - return { - result: false, - reason: "Too long", - }; - } - } - - if (config.noSpace && text.includes(" ")) { - return { - result: false, - reason: "Don't allow space", - }; - } - - if (config.noChinese && chineseReg.test(text)) { - return { - result: false, - reason: "Don't allow chinese", - }; - } - - return { - result: true, - }; -} diff --git a/web/src/pages/Account.tsx b/web/src/pages/Account.tsx new file mode 100644 index 0000000..3487257 --- /dev/null +++ b/web/src/pages/Account.tsx @@ -0,0 +1,37 @@ +import { Button } from "@mui/joy"; +import { useState } from "react"; +import { useAppSelector } from "../stores"; +import ChangePasswordDialog from "../components/ChangePasswordDialog"; +import EditUserinfoDialog from "../components/EditUserinfoDialog"; + +const Account: React.FC = () => { + const user = useAppSelector((state) => state.user).user as User; + const [showEditUserinfoDialog, setShowEditUserinfoDialog] = useState(false); + const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false); + + return ( + <> +
+

{user.nickname}

+

+ Email: + {user.email} +

+
+ + +
+
+ + {showEditUserinfoDialog && setShowEditUserinfoDialog(false)} />} + + {showChangePasswordDialog && setShowChangePasswordDialog(false)} />} + + ); +}; + +export default Account; diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index 04a5fd5..b95459f 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -14,10 +14,7 @@ const Auth: React.FC = () => { const actionBtnLoadingState = useLoading(false); useEffect(() => { - if (userService.getState().user) { - navigate("/"); - return; - } + userService.doSignOut(); }, []); const handleEmailInputChanged = (e: React.ChangeEvent) => { diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 794442f..97e8e5c 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { userService, shortcutService } from "../services"; +import { shortcutService } from "../services"; import { useAppSelector } from "../stores"; import useLoading from "../hooks/useLoading"; import Icon from "../components/Icon"; @@ -13,7 +12,6 @@ interface State { } const Home: React.FC = () => { - const navigate = useNavigate(); const loadingState = useLoading(); const { shortcutList } = useAppSelector((state) => state.shortcut); const [state, setState] = useState({ @@ -21,11 +19,6 @@ const Home: React.FC = () => { }); useEffect(() => { - if (!userService.getState().user) { - navigate("/user/auth"); - return; - } - Promise.all([shortcutService.getMyAllShortcuts()]).finally(() => { loadingState.setFinish(); }); diff --git a/web/src/pages/UserDetail.tsx b/web/src/pages/UserDetail.tsx deleted file mode 100644 index b7d1330..0000000 --- a/web/src/pages/UserDetail.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Button } from "@mui/joy"; -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { useAppSelector } from "../stores"; -import { userService } from "../services"; -import ChangePasswordDialog from "../components/ChangePasswordDialog"; - -interface State { - showChangePasswordDialog: boolean; -} - -const UserDetail: React.FC = () => { - const navigate = useNavigate(); - const { user } = useAppSelector((state) => state.user); - const [state, setState] = useState({ - showChangePasswordDialog: false, - }); - - useEffect(() => { - if (!userService.getState().user) { - navigate("/user/auth"); - return; - } - }, []); - - const handleChangePasswordBtnClick = async () => { - setState({ - ...state, - showChangePasswordDialog: true, - }); - }; - - return ( - <> -
-

{user?.nickname}

-

- Email: - {user?.email} -

-
- Password: - -
-
- - {state.showChangePasswordDialog && ( - { - setState({ - ...state, - showChangePasswordDialog: false, - }); - }} - /> - )} - - ); -}; - -export default UserDetail; diff --git a/web/src/routers/index.tsx b/web/src/routers/index.tsx index d445833..3210fc1 100644 --- a/web/src/routers/index.tsx +++ b/web/src/routers/index.tsx @@ -4,11 +4,11 @@ import { userService } from "../services"; import Root from "../layouts/Root"; import Auth from "../pages/Auth"; import Home from "../pages/Home"; -import UserDetail from "../pages/UserDetail"; +import Account from "../pages/Account"; const router = createBrowserRouter([ { - path: "/user/auth", + path: "/auth", element: , }, { @@ -27,14 +27,14 @@ const router = createBrowserRouter([ const { user } = userService.getState(); if (isNullorUndefined(user)) { - return redirect("/user/auth"); + return redirect("/auth"); } return null; }, }, { path: "/account", - element: , + element: , loader: async () => { try { await userService.initialState(); @@ -44,7 +44,7 @@ const router = createBrowserRouter([ const { user } = userService.getState(); if (isNullorUndefined(user)) { - return redirect("/user/auth"); + return redirect("/auth"); } return null; }, diff --git a/web/src/types/modules/user.d.ts b/web/src/types/modules/user.d.ts index 712c517..5f2757b 100644 --- a/web/src/types/modules/user.d.ts +++ b/web/src/types/modules/user.d.ts @@ -18,9 +18,9 @@ interface UserPatch { id: UserId; rowStatus?: RowStatus; - displayName?: string; + email?: string; + nickname?: string; password?: string; - resetOpenId?: boolean; } interface UserDelete {