From 19265fb44fe0970e0a6c9afe8f2b48571aee9e75 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 15 Feb 2021 01:23:24 +0800 Subject: feat: Move change avatar and nickname to settings. --- FrontEnd/src/app/locales/en/translation.json | 37 ++- FrontEnd/src/app/locales/zh/translation.json | 37 ++- .../src/app/views/settings/ChangeAvatarDialog.tsx | 307 +++++++++++++++++++++ .../app/views/settings/ChangeNicknameDialog.tsx | 32 +++ .../app/views/settings/ChangePasswordDialog.tsx | 68 +++++ FrontEnd/src/app/views/settings/index.tsx | 94 ++----- FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx | 302 -------------------- .../src/app/views/user/ChangeNicknameDialog.tsx | 27 -- FrontEnd/src/app/views/user/UserCard.tsx | 52 +--- 9 files changed, 466 insertions(+), 490 deletions(-) create mode 100644 FrontEnd/src/app/views/settings/ChangeAvatarDialog.tsx create mode 100644 FrontEnd/src/app/views/settings/ChangeNicknameDialog.tsx create mode 100644 FrontEnd/src/app/views/settings/ChangePasswordDialog.tsx delete mode 100644 FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx delete mode 100644 FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json index 274d629b..63b2a1be 100644 --- a/FrontEnd/src/app/locales/en/translation.json +++ b/FrontEnd/src/app/locales/en/translation.json @@ -129,24 +129,6 @@ "badCredential": "Username or password is invalid.", "alreadyLogin": "Already login! Redirect to home page in 3s!" }, - "userPage": { - "dialogChangeNickname": { - "title": "Change Nickname", - "inputLabel": "New nickname" - }, - "dialogChangeAvatar": { - "title": "Change Avatar", - "previewImgAlt": "preview", - "prompt": { - "select": "Please select a picture.", - "crop": "Please crop the picture.", - "processingCrop": "Cropping picture...", - "uploading": "Uploading...", - "preview": "Please preview avatar" - }, - "upload": "upload" - } - }, "settings": { "subheaders": { "account": "Account", @@ -156,7 +138,8 @@ "languageSecondary": "You language preference will be saved locally. Next time you visit this page, last language option will be used.", "changePassword": "Change account's password.", "logout": "Log out this account.", - "gotoSelf": "Click here to go to timeline of myself to change nickname and avatar.", + "changeAvatar": "Change avatar.", + "changeNickname": "Change nickname.", "dialogChangePassword": { "title": "Change Password", "prompt": "You are changing your password. You need to input the correct old password. After change, you need to login again and all old login will be invalid.", @@ -170,6 +153,22 @@ "dialogConfirmLogout": { "title": "Confirm Logout", "prompt": "Are you sure to log out? All cached data in the browser will be deleted." + }, + "dialogChangeNickname": { + "title": "Change Nickname", + "inputLabel": "New nickname" + }, + "dialogChangeAvatar": { + "title": "Change Avatar", + "previewImgAlt": "preview", + "prompt": { + "select": "Please select a picture.", + "crop": "Please crop the picture.", + "processingCrop": "Cropping picture...", + "uploading": "Uploading...", + "preview": "Please preview avatar" + }, + "upload": "upload" } }, "about": { diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json index 759dc63c..296966c4 100644 --- a/FrontEnd/src/app/locales/zh/translation.json +++ b/FrontEnd/src/app/locales/zh/translation.json @@ -129,24 +129,6 @@ "badCredential": "用户名或密码错误。", "alreadyLogin": "已经登陆,三秒后导航到首页!" }, - "userPage": { - "dialogChangeNickname": { - "title": "更改昵称", - "inputLabel": "新昵称" - }, - "dialogChangeAvatar": { - "title": "修改头像", - "previewImgAlt": "预览", - "prompt": { - "select": "请选择一个图片", - "crop": "请裁剪图片", - "processingCrop": "正在裁剪图片", - "uploading": "正在上传", - "preview": "请预览图片" - }, - "upload": "上传" - } - }, "settings": { "subheaders": { "account": "账户", @@ -156,7 +138,8 @@ "languageSecondary": "您的语言偏好将会存储在本地,下次浏览时将自动使用上次保存的语言选项。", "changePassword": "更改账号的密码。", "logout": "注销此账号。", - "gotoSelf": "点击前往个人时间线修改昵称和头像!", + "changeAvatar": "更改头像。", + "changeNickname": "更改昵称。", "dialogChangePassword": { "title": "修改密码", "prompt": "您正在修改密码,您需要输入正确的旧密码。成功修改后您需要重新登陆,而且以前所有的登录都会失效。", @@ -170,6 +153,22 @@ "dialogConfirmLogout": { "title": "确定注销", "prompt": "您确定注销此账号?这将删除所有已经缓存在浏览器的数据。" + }, + "dialogChangeNickname": { + "title": "更改昵称", + "inputLabel": "新昵称" + }, + "dialogChangeAvatar": { + "title": "修改头像", + "previewImgAlt": "预览", + "prompt": { + "select": "请选择一个图片", + "crop": "请裁剪图片", + "processingCrop": "正在裁剪图片", + "uploading": "正在上传", + "preview": "请预览图片" + }, + "upload": "上传" } }, "about": { diff --git a/FrontEnd/src/app/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/app/views/settings/ChangeAvatarDialog.tsx new file mode 100644 index 00000000..53ffbc8d --- /dev/null +++ b/FrontEnd/src/app/views/settings/ChangeAvatarDialog.tsx @@ -0,0 +1,307 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { AxiosError } from "axios"; +import { Modal, Row, Button } from "react-bootstrap"; + +import { UiLogicError } from "@/common"; + +import { useUserLoggedIn } from "@/services/user"; + +import { getHttpUserClient } from "@/http/user"; + +import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; + +export interface ChangeAvatarDialogProps { + open: boolean; + close: () => void; +} + +const ChangeAvatarDialog: React.FC = (props) => { + const { t } = useTranslation(); + + const user = useUserLoggedIn(); + + const [file, setFile] = React.useState(null); + const [fileUrl, setFileUrl] = React.useState(null); + const [clip, setClip] = React.useState(null); + const [ + cropImgElement, + setCropImgElement, + ] = React.useState(null); + const [resultBlob, setResultBlob] = React.useState(null); + const [resultUrl, setResultUrl] = React.useState(null); + + const [state, setState] = React.useState< + | "select" + | "crop" + | "processcrop" + | "preview" + | "uploading" + | "success" + | "error" + >("select"); + + const [message, setMessage] = useState< + string | { type: "custom"; text: string } | null + >("settings.dialogChangeAvatar.prompt.select"); + + const trueMessage = + message == null + ? null + : typeof message === "string" + ? t(message) + : message.text; + + const closeDialog = props.close; + + const close = React.useCallback((): void => { + if (!(state === "uploading")) { + closeDialog(); + } + }, [state, closeDialog]); + + useEffect(() => { + if (file != null) { + const url = URL.createObjectURL(file); + setClip(null); + setFileUrl(url); + setState("crop"); + return () => { + URL.revokeObjectURL(url); + }; + } else { + setFileUrl(null); + setState("select"); + } + }, [file]); + + React.useEffect(() => { + if (resultBlob != null) { + const url = URL.createObjectURL(resultBlob); + setResultUrl(url); + setState("preview"); + return () => { + URL.revokeObjectURL(url); + }; + } else { + setResultUrl(null); + } + }, [resultBlob]); + + const onSelectFile = React.useCallback( + (e: React.ChangeEvent): void => { + const files = e.target.files; + if (files == null || files.length === 0) { + setFile(null); + } else { + setFile(files[0]); + } + }, + [] + ); + + const onCropNext = React.useCallback(() => { + if ( + cropImgElement == null || + clip == null || + clip.width === 0 || + file == null + ) { + throw new UiLogicError(); + } + + setState("processcrop"); + void applyClipToImage(cropImgElement, clip, file.type).then((b) => { + setResultBlob(b); + }); + }, [cropImgElement, clip, file]); + + const onCropPrevious = React.useCallback(() => { + setFile(null); + setState("select"); + }, []); + + const onPreviewPrevious = React.useCallback(() => { + setResultBlob(null); + setState("crop"); + }, []); + + const upload = React.useCallback(() => { + if (resultBlob == null) { + throw new UiLogicError(); + } + + setState("uploading"); + getHttpUserClient() + .putAvatar(user.username, resultBlob) + .then( + () => { + setState("success"); + }, + (e: unknown) => { + setState("error"); + setMessage({ type: "custom", text: (e as AxiosError).message }); + } + ); + }, [user.username, resultBlob]); + + const createPreviewRow = (): React.ReactElement => { + if (resultUrl == null) { + throw new UiLogicError(); + } + return ( + + {t("settings.dialogChangeAvatar.previewImgAlt")} + + ); + }; + + return ( + + + {t("settings.dialogChangeAvatar.title")} + + {(() => { + if (state === "select") { + return ( + <> + + {t("settings.dialogChangeAvatar.prompt.select")} + + + + + + + + + ); + } else if (state === "crop") { + if (fileUrl == null) { + throw new UiLogicError(); + } + return ( + <> + + + + + {t("settings.dialogChangeAvatar.prompt.crop")} + + + + + + + + ); + } else if (state === "processcrop") { + return ( + <> + + + {t("settings.dialogChangeAvatar.prompt.processingCrop")} + + + + + + + + ); + } else if (state === "preview") { + return ( + <> + + {createPreviewRow()} + {t("settings.dialogChangeAvatar.prompt.preview")} + + + + + + + + ); + } else if (state === "uploading") { + return ( + <> + + {createPreviewRow()} + {t("settings.dialogChangeAvatar.prompt.uploading")} + + + + ); + } else if (state === "success") { + return ( + <> + + + {t("operationDialog.success")} + + + + + + + ); + } else { + return ( + <> + + {createPreviewRow()} + {trueMessage} + + + + + + + ); + } + })()} + + ); +}; + +export default ChangeAvatarDialog; diff --git a/FrontEnd/src/app/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/app/views/settings/ChangeNicknameDialog.tsx new file mode 100644 index 00000000..4b44cdd6 --- /dev/null +++ b/FrontEnd/src/app/views/settings/ChangeNicknameDialog.tsx @@ -0,0 +1,32 @@ +import { getHttpUserClient } from "@/http/user"; +import { useUserLoggedIn } from "@/services/user"; +import React from "react"; + +import OperationDialog from "../common/OperationDialog"; + +export interface ChangeNicknameDialogProps { + open: boolean; + close: () => void; +} + +const ChangeNicknameDialog: React.FC = (props) => { + const user = useUserLoggedIn(); + + return ( + { + return getHttpUserClient().patch(user.username, { + nickname: newNickname, + }); + }} + close={props.close} + /> + ); +}; + +export default ChangeNicknameDialog; diff --git a/FrontEnd/src/app/views/settings/ChangePasswordDialog.tsx b/FrontEnd/src/app/views/settings/ChangePasswordDialog.tsx new file mode 100644 index 00000000..21eeeb09 --- /dev/null +++ b/FrontEnd/src/app/views/settings/ChangePasswordDialog.tsx @@ -0,0 +1,68 @@ +import React, { useState } from "react"; +import { useHistory } from "react-router"; + +import { userService } from "@/services/user"; + +import OperationDialog from "../common/OperationDialog"; + +export interface ChangePasswordDialogProps { + open: boolean; + close: () => void; +} + +const ChangePasswordDialog: React.FC = (props) => { + const history = useHistory(); + + const [redirect, setRedirect] = useState(false); + + return ( + { + const result: Record = {}; + if (oldPassword === "") { + result[0] = "settings.dialogChangePassword.errorEmptyOldPassword"; + } + if (newPassword === "") { + result[1] = "settings.dialogChangePassword.errorEmptyNewPassword"; + } + if (retypedNewPassword !== newPassword) { + result[2] = "settings.dialogChangePassword.errorRetypeNotMatch"; + } + return result; + }} + onProcess={async ([oldPassword, newPassword]) => { + await userService.changePassword(oldPassword, newPassword); + setRedirect(true); + }} + close={() => { + props.close(); + if (redirect) { + history.push("/login"); + } + }} + /> + ); +}; + +export default ChangePasswordDialog; diff --git a/FrontEnd/src/app/views/settings/index.tsx b/FrontEnd/src/app/views/settings/index.tsx index ccba59b7..6710ea25 100644 --- a/FrontEnd/src/app/views/settings/index.tsx +++ b/FrontEnd/src/app/views/settings/index.tsx @@ -4,67 +4,10 @@ import { useTranslation } from "react-i18next"; import { Container, Form, Row, Col, Button, Modal } from "react-bootstrap"; import { useUser, userService } from "@/services/user"; -import OperationDialog from "../common/OperationDialog"; -interface ChangePasswordDialogProps { - open: boolean; - close: () => void; -} - -const ChangePasswordDialog: React.FC = (props) => { - const history = useHistory(); - - const [redirect, setRedirect] = useState(false); - - return ( - { - const result: Record = {}; - if (oldPassword === "") { - result[0] = "settings.dialogChangePassword.errorEmptyOldPassword"; - } - if (newPassword === "") { - result[1] = "settings.dialogChangePassword.errorEmptyNewPassword"; - } - if (retypedNewPassword !== newPassword) { - result[2] = "settings.dialogChangePassword.errorRetypeNotMatch"; - } - return result; - }} - onProcess={async ([oldPassword, newPassword]) => { - await userService.changePassword(oldPassword, newPassword); - setRedirect(true); - }} - close={() => { - props.close(); - if (redirect) { - history.push("/login"); - } - }} - /> - ); -}; +import ChangePasswordDialog from "./ChangePasswordDialog"; +import ChangeAvatarDialog from "./ChangeAvatarDialog"; +import ChangeNicknameDialog from "./ChangeNicknameDialog"; const ConfirmLogoutDialog: React.FC<{ onClose: () => void; @@ -97,9 +40,9 @@ const SettingsPage: React.FC = (_) => { const user = useUser(); const history = useHistory(); - const [dialog, setDialog] = useState( - null - ); + const [dialog, setDialog] = useState< + null | "changepassword" | "changeavatar" | "changenickname" | "logout" + >(null); const language = i18n.language.slice(0, 2); @@ -113,11 +56,15 @@ const SettingsPage: React.FC = (_) => {
{ - history.push(`/users/${user.username}`); - }} + onClick={() => setDialog("changeavatar")} > - {t("settings.gotoSelf")} + {t("settings.changeAvatar")} +
+
setDialog("changenickname")} + > + {t("settings.changeNickname")}
{ {(() => { switch (dialog) { case "changepassword": - return ( - { - setDialog(null); - }} - /> - ); + return setDialog(null)} />; case "logout": return ( { }} /> ); + case "changeavatar": + return setDialog(null)} />; + case "changenickname": + return setDialog(null)} />; default: return null; } diff --git a/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx b/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx deleted file mode 100644 index ffa2218b..00000000 --- a/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { AxiosError } from "axios"; -import { Modal, Row, Button } from "react-bootstrap"; - -import { UiLogicError } from "@/common"; - -import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; - -export interface ChangeAvatarDialogProps { - open: boolean; - close: () => void; - process: (blob: Blob) => Promise; -} - -const ChangeAvatarDialog: React.FC = (props) => { - const { t } = useTranslation(); - - const [file, setFile] = React.useState(null); - const [fileUrl, setFileUrl] = React.useState(null); - const [clip, setClip] = React.useState(null); - const [ - cropImgElement, - setCropImgElement, - ] = React.useState(null); - const [resultBlob, setResultBlob] = React.useState(null); - const [resultUrl, setResultUrl] = React.useState(null); - - const [state, setState] = React.useState< - | "select" - | "crop" - | "processcrop" - | "preview" - | "uploading" - | "success" - | "error" - >("select"); - - const [message, setMessage] = useState< - string | { type: "custom"; text: string } | null - >("userPage.dialogChangeAvatar.prompt.select"); - - const trueMessage = - message == null - ? null - : typeof message === "string" - ? t(message) - : message.text; - - const closeDialog = props.close; - - const close = React.useCallback((): void => { - if (!(state === "uploading")) { - closeDialog(); - } - }, [state, closeDialog]); - - useEffect(() => { - if (file != null) { - const url = URL.createObjectURL(file); - setClip(null); - setFileUrl(url); - setState("crop"); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setFileUrl(null); - setState("select"); - } - }, [file]); - - React.useEffect(() => { - if (resultBlob != null) { - const url = URL.createObjectURL(resultBlob); - setResultUrl(url); - setState("preview"); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setResultUrl(null); - } - }, [resultBlob]); - - const onSelectFile = React.useCallback( - (e: React.ChangeEvent): void => { - const files = e.target.files; - if (files == null || files.length === 0) { - setFile(null); - } else { - setFile(files[0]); - } - }, - [] - ); - - const onCropNext = React.useCallback(() => { - if ( - cropImgElement == null || - clip == null || - clip.width === 0 || - file == null - ) { - throw new UiLogicError(); - } - - setState("processcrop"); - void applyClipToImage(cropImgElement, clip, file.type).then((b) => { - setResultBlob(b); - }); - }, [cropImgElement, clip, file]); - - const onCropPrevious = React.useCallback(() => { - setFile(null); - setState("select"); - }, []); - - const onPreviewPrevious = React.useCallback(() => { - setResultBlob(null); - setState("crop"); - }, []); - - const process = props.process; - - const upload = React.useCallback(() => { - if (resultBlob == null) { - throw new UiLogicError(); - } - - setState("uploading"); - process(resultBlob).then( - () => { - setState("success"); - }, - (e: unknown) => { - setState("error"); - setMessage({ type: "custom", text: (e as AxiosError).message }); - } - ); - }, [resultBlob, process]); - - const createPreviewRow = (): React.ReactElement => { - if (resultUrl == null) { - throw new UiLogicError(); - } - return ( - - {t("userPage.dialogChangeAvatar.previewImgAlt")} - - ); - }; - - return ( - - - {t("userPage.dialogChangeAvatar.title")} - - {(() => { - if (state === "select") { - return ( - <> - - {t("userPage.dialogChangeAvatar.prompt.select")} - - - - - - - - - ); - } else if (state === "crop") { - if (fileUrl == null) { - throw new UiLogicError(); - } - return ( - <> - - - - - {t("userPage.dialogChangeAvatar.prompt.crop")} - - - - - - - - ); - } else if (state === "processcrop") { - return ( - <> - - - {t("userPage.dialogChangeAvatar.prompt.processingCrop")} - - - - - - - - ); - } else if (state === "preview") { - return ( - <> - - {createPreviewRow()} - {t("userPage.dialogChangeAvatar.prompt.preview")} - - - - - - - - ); - } else if (state === "uploading") { - return ( - <> - - {createPreviewRow()} - {t("userPage.dialogChangeAvatar.prompt.uploading")} - - - - ); - } else if (state === "success") { - return ( - <> - - - {t("operationDialog.success")} - - - - - - - ); - } else { - return ( - <> - - {createPreviewRow()} - {trueMessage} - - - - - - - ); - } - })()} - - ); -}; - -export default ChangeAvatarDialog; diff --git a/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx b/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx deleted file mode 100644 index f319ac37..00000000 --- a/FrontEnd/src/app/views/user/ChangeNicknameDialog.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; - -import OperationDialog from "../common/OperationDialog"; - -export interface ChangeNicknameDialogProps { - open: boolean; - close: () => void; - onProcess: (newNickname: string) => Promise; -} - -const ChangeNicknameDialog: React.FC = (props) => { - return ( - { - return props.onProcess(newNickname); - }} - close={props.close} - /> - ); -}; - -export default ChangeNicknameDialog; diff --git a/FrontEnd/src/app/views/user/UserCard.tsx b/FrontEnd/src/app/views/user/UserCard.tsx index 575ca2c1..b2c94457 100644 --- a/FrontEnd/src/app/views/user/UserCard.tsx +++ b/FrontEnd/src/app/views/user/UserCard.tsx @@ -5,16 +5,13 @@ import TimelinePageCardTemplate, { } from "../timeline-common/TimelinePageCardTemplate"; import { TimelinePageCardProps } from "../timeline-common/TimelinePageTemplate"; import UserAvatar from "../common/user/UserAvatar"; -import ChangeNicknameDialog from "./ChangeNicknameDialog"; -import { getHttpUserClient } from "@/http/user"; -import ChangeAvatarDialog from "./ChangeAvatarDialog"; const UserCard: React.FC = (props) => { - const { timeline, onReload } = props; + const { timeline } = props; - const [dialog, setDialog] = React.useState< - "member" | "property" | "avatar" | "nickname" | null - >(null); + const [dialog, setDialog] = React.useState<"member" | "property" | null>( + null + ); return ( <> @@ -41,16 +38,6 @@ const UserCard: React.FC = (props) => { return { type: "manage", items: [ - { - type: "button", - text: "timeline.manageItem.nickname", - onClick: () => setDialog("nickname"), - }, - { - type: "button", - text: "timeline.manageItem.avatar", - onClick: () => setDialog("avatar"), - }, { type: "button", text: "timeline.manageItem.property", @@ -69,37 +56,6 @@ const UserCard: React.FC = (props) => { setDialog={setDialog} {...props} /> - {(() => { - // TODO: Move this two to settings. - if (dialog === "nickname") { - return ( - setDialog(null)} - onProcess={async (newNickname) => { - await getHttpUserClient().patch(timeline.owner.username, { - nickname: newNickname, - }); - onReload(); - }} - /> - ); - } else if (dialog === "avatar") { - return ( - setDialog(null)} - process={async (file) => { - await getHttpUserClient().putAvatar( - timeline.owner.username, - file - ); - onReload(); - }} - /> - ); - } - })()} ); }; -- cgit v1.2.3