diff options
-rw-r--r-- | FrontEnd/src/common.ts | 1 | ||||
-rw-r--r-- | FrontEnd/src/locales/en/translation.json | 3 | ||||
-rw-r--r-- | FrontEnd/src/locales/zh/translation.json | 2 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/index.tsx | 279 | ||||
-rw-r--r-- | FrontEnd/src/views/common/Card.tsx | 2 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/Button.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/FlatButton.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/views/common/common.ts | 2 |
8 files changed, 133 insertions, 164 deletions
diff --git a/FrontEnd/src/common.ts b/FrontEnd/src/common.ts index 965f9933..7c053140 100644 --- a/FrontEnd/src/common.ts +++ b/FrontEnd/src/common.ts @@ -6,5 +6,6 @@ export class UiLogicError extends Error {} export const highlightTimelineUsername = "crupest"; export type { I18nText } from "./i18n"; +export type { I18nText as Text } from "./i18n"; export { c, convertI18nText } from "./i18n"; export { default as useC } from "./utilities/hooks/use-c"; diff --git a/FrontEnd/src/locales/en/translation.json b/FrontEnd/src/locales/en/translation.json index 95c722c9..a73472d2 100644 --- a/FrontEnd/src/locales/en/translation.json +++ b/FrontEnd/src/locales/en/translation.json @@ -176,7 +176,7 @@ "noAccount": "If you don't have an account and know a register code, then click <1>here</1> to register." }, "settings": { - "subheaders": { + "subheader": { "account": "Account", "customization": "Customization" }, @@ -186,7 +186,6 @@ "logout": "Log out this account", "changeAvatar": "Change avatar", "changeNickname": "Change nickname", - "changeBookmarkVisibility": "Change bookmark visibility", "myRegisterCode": "My register code:", "myRegisterCodeDesc": "Click to create a new register code.", "renewRegisterCode": "Renew Register Code", diff --git a/FrontEnd/src/locales/zh/translation.json b/FrontEnd/src/locales/zh/translation.json index b7212128..8a2f628f 100644 --- a/FrontEnd/src/locales/zh/translation.json +++ b/FrontEnd/src/locales/zh/translation.json @@ -176,7 +176,7 @@ "noAccount": "如果你没有账号但有一个注册码,请点击<1>这里</1>注册账号。" }, "settings": { - "subheaders": { + "subheader": { "account": "账户", "customization": "个性化" }, diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 00503dcf..4e28585e 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -1,16 +1,21 @@ -import { useState, ReactNode } from "react"; -import { useNavigate } from "react-router-dom"; +import { + useState, + useEffect, + ReactNode, + ComponentPropsWithoutRef, +} from "react"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; import classNames from "classnames"; -import { useC, I18nText } from "@/common"; +import { useC, Text } from "@/common"; import { useUser, userService } from "@/services/user"; import { getHttpUserClient } from "@/http/user"; import { TimelineVisibility } from "@/http/timeline"; -import ConfirmDialog from "../common/dialog/ConfirmDialog"; -import Card from "../common/Card"; -import Spinner from "../common/Spinner"; +import ConfirmDialog from "@/views/common/dialog/ConfirmDialog"; +import Card from "@/views/common/Card"; +import Spinner from "@/views/common/Spinner"; import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; @@ -18,99 +23,94 @@ import ChangeNicknameDialog from "./ChangeNicknameDialog"; import "./index.css"; import { pushAlert } from "@/services/alert"; -interface SettingSectionProps { - title: I18nText; +interface SettingSectionProps + extends Omit<ComponentPropsWithoutRef<"div">, "title"> { + title: Text; children: ReactNode; } -function SettingSection({ title, children }: SettingSectionProps) { +function SettingSection({ + title, + className, + children, + ...otherProps +}: SettingSectionProps) { const c = useC(); return ( - <Card> - <h2 className="">{c(title)}</h2> + <Card className={classNames(className, "setting-section")} {...otherProps}> + <h2 className="setting-section-title">{c(title)}</h2> {children} </Card> ); } -interface SettingItemContainerWithoutChildrenProps { - title: I18nText; - subtext?: I18nText; - first?: boolean; - danger?: boolean; - style?: React.CSSProperties; - className?: string; - onClick?: () => void; -} - interface SettingItemContainerProps - extends SettingItemContainerWithoutChildrenProps { - children?: React.ReactNode; + extends Omit<ComponentPropsWithoutRef<"div">, "title"> { + title: Text; + description?: Text; + danger?: boolean; + extraClassName?: string; } function SettingItemContainer({ title, - subtext, - first, + description, danger, - children, - style, + extraClassName, className, - onClick, -}: SettingItemContainerProps): JSX.Element { - const { t } = useTranslation(); + children, + ...otherProps +}: SettingItemContainerProps) { + const c = useC(); return ( <div - style={style} className={classNames( - "row settings-item mx-0", - first && "first", - onClick && "clickable", className, + "setting-item-container", + danger && "danger", + extraClassName, )} - onClick={onClick} + {...otherProps} > - <div className="px-0 col col-auto"> - <div className={classNames(danger && "cru-color-danger")}> - {convertI18nText(title, t)} - </div> - <small className="d-block cru-color-secondary"> - {convertI18nText(subtext, t)} - </small> + <div className="setting-item-label-area"> + <div className="setting-item-label-title">{c(title)}</div> + <small className="setting-item-label-sub">{c(description)}</small> </div> - <div className="col col-auto">{children}</div> + <div className="setting-item-value-area">{children}</div> </div> ); } -type ButtonSettingItemProps = SettingItemContainerWithoutChildrenProps; +type ButtonSettingItemProps = Omit<SettingItemContainerProps, "extraClassName">; -const ButtonSettingItem: React.FC<ButtonSettingItemProps> = ({ ...props }) => { - return <SettingItemContainer {...props} />; -}; +function ButtonSettingItem(props: ButtonSettingItemProps) { + return ( + <SettingItemContainer extraClassName="setting-type-button" {...props} /> + ); +} interface SelectSettingItemProps - extends SettingItemContainerWithoutChildrenProps { + extends Omit<SettingItemContainerProps, "onSelect" | "extraClassName"> { options: { value: string; - label: I18nText; + label: Text; }[]; - value?: string; + value?: string | null; onSelect: (value: string) => void; } -const SelectSettingsItem: React.FC<SelectSettingItemProps> = ({ +function SelectSettingsItem({ options, value, onSelect, - ...props -}) => { - const { t } = useTranslation(); + ...extraProps +}: SelectSettingItemProps) { + const c = useC(); return ( - <SettingItemContainer {...props}> + <SettingItemContainer extraClassName="setting-type-select" {...extraProps}> {value == null ? ( <Spinner /> ) : ( @@ -122,53 +122,30 @@ const SelectSettingsItem: React.FC<SelectSettingItemProps> = ({ > {options.map(({ value, label }) => ( <option key={value} value={value}> - {convertI18nText(label, t)} + {c(label)} </option> ))} </select> )} </SettingItemContainer> ); -}; +} -const SettingsPage: React.FC = () => { - const { i18n } = useTranslation(); +function RegisterCodeSettingItem({ + openRenewDialog, +}: { + openRenewDialog: () => void; +}) { const user = useUser(); - const navigate = useNavigate(); - - const [dialog, setDialog] = useState< - | null - | "changepassword" - | "changeavatar" - | "changenickname" - | "logout" - | "renewregistercode" - >(null); - const [registerCode, setRegisterCode] = useState<undefined | null | string>( - undefined, - ); - - const [bookmarkVisibility, setBookmarkVisibility] = - useState<TimelineVisibility>(); - - React.useEffect(() => { - if (user != null) { - void getHttpUserClient() - .getBookmarkVisibility(user.username) - .then(({ visibility }) => { - setBookmarkVisibility(visibility); - }); - } else { - setBookmarkVisibility(undefined); - } - }, [user]); + // undefined: loading + const [registerCode, setRegisterCode] = useState<undefined | null | string>(); - React.useEffect(() => { + useEffect(() => { setRegisterCode(undefined); }, [user]); - React.useEffect(() => { + useEffect(() => { if (user != null && registerCode === undefined) { void getHttpUserClient() .getRegisterCode(user.username) @@ -178,87 +155,81 @@ const SettingsPage: React.FC = () => { } }, [user, registerCode]); + return ( + <SettingItemContainer + title="settings.myRegisterCode" + description="settings.myRegisterCodeDesc" + onClick={openRenewDialog} + > + {registerCode === undefined ? ( + <Spinner /> + ) : registerCode === null ? ( + <span>Noop</span> + ) : ( + <code + className="register-code" + onClick={(event) => { + void navigator.clipboard.writeText(registerCode).then(() => { + pushAlert({ + type: "success", + message: "settings.myRegisterCodeCopied", + }); + }); + event.stopPropagation(); + }} + > + {registerCode} + </code> + )} + </SettingItemContainer> + ); +} + +export default function SettingsPage() { + const c = useC(); + const { i18n } = useTranslation(); + const user = useUser(); + const navigate = useNavigate(); + + type DialogName = + | "change-password" + | "change-avatar" + | "change-nickname" + | "logout" + | "renew-register-code"; + + const [dialog, setDialog] = useState<null | DialogName>(null); + + function dialogOpener(name: DialogName): () => void { + return () => setDialog(name); + } + const language = i18n.language.slice(0, 2); return ( <> <div className="container"> {user ? ( - <SettingSection title="settings.subheaders.account"> - <SettingItemContainer - title="settings.myRegisterCode" - subtext="settings.myRegisterCodeDesc" - onClick={() => setDialog("renewregistercode")} - > - {registerCode === undefined ? ( - <Spinner /> - ) : registerCode === null ? ( - <span>Noop</span> - ) : ( - <code - className="register-code" - onClick={(event) => { - void navigator.clipboard - .writeText(registerCode) - .then(() => { - pushAlert({ - type: "success", - message: "settings.myRegisterCodeCopied", - }); - }); - event.stopPropagation(); - }} - > - {registerCode} - </code> - )} - </SettingItemContainer> + <SettingSection title="settings.subheader.account"> + <RegisterCodeSettingItem + openRenewDialog={dialogOpener("renew-register-code")} + /> <ButtonSettingItem title="settings.changeAvatar" - onClick={() => setDialog("changeavatar")} - first + onClick={dialogOpener("change-avatar")} /> <ButtonSettingItem title="settings.changeNickname" - onClick={() => setDialog("changenickname")} - /> - <SelectSettingsItem - title="settings.changeBookmarkVisibility" - options={[ - { - value: "Private", - label: "visibility.private", - }, - { - value: "Register", - label: "visibility.register", - }, - { - value: "Public", - label: "visibility.public", - }, - ]} - value={bookmarkVisibility} - onSelect={(value) => { - void getHttpUserClient() - .putBookmarkVisibility(user.username, { - visibility: value as TimelineVisibility, - }) - .then(() => { - setBookmarkVisibility(value as TimelineVisibility); - }); - }} + onClick={dialogOpener("change-nickname")} /> <ButtonSettingItem title="settings.changePassword" - onClick={() => setDialog("changepassword")} + onClick={dialogOpener("change-password")} danger /> <ButtonSettingItem title="settings.logout" - onClick={() => { - setDialog("logout"); - }} + onClick={dialogOpener("logout")} danger /> </SettingSection> @@ -330,6 +301,4 @@ const SettingsPage: React.FC = () => { /> </> ); -}; - -export default SettingsPage; +} diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx index 50632006..5ff89b61 100644 --- a/FrontEnd/src/views/common/Card.tsx +++ b/FrontEnd/src/views/common/Card.tsx @@ -4,7 +4,7 @@ import classNames from "classnames"; import "./Card.css"; interface CardProps extends ComponentPropsWithoutRef<"div"> { - containerRef: Ref<HTMLDivElement>; + containerRef?: Ref<HTMLDivElement> | null; } export default function Card({ diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx index e1015f71..0f1bbf2b 100644 --- a/FrontEnd/src/views/common/button/Button.tsx +++ b/FrontEnd/src/views/common/button/Button.tsx @@ -1,13 +1,13 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; -import { I18nText, useC, ThemeColor } from "../common"; +import { Text, useC, ThemeColor } from "../common"; import "./Button.css"; interface ButtonProps extends ComponentPropsWithoutRef<"button"> { color?: ThemeColor; - text?: I18nText; + text?: Text; outline?: boolean; buttonRef?: Ref<HTMLButtonElement> | null; } diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index 7b268b6d..ed01f613 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -1,13 +1,13 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; -import { I18nText, useC, ThemeColor } from "../common"; +import { Text, useC, ThemeColor } from "../common"; import "./FlatButton.css"; interface FlatButtonProps extends ComponentPropsWithoutRef<"button"> { color?: ThemeColor; - text?: I18nText; + text?: Text; buttonRef?: Ref<HTMLButtonElement> | null; } diff --git a/FrontEnd/src/views/common/common.ts b/FrontEnd/src/views/common/common.ts index d3db9f93..4ad41edc 100644 --- a/FrontEnd/src/views/common/common.ts +++ b/FrontEnd/src/views/common/common.ts @@ -1,4 +1,4 @@ -export type { I18nText } from "@/common"; +export type { Text, I18nText } from "@/common"; export { c, convertI18nText, useC } from "@/common"; export const themeColors = [ |