aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FrontEnd/src/common.ts1
-rw-r--r--FrontEnd/src/locales/en/translation.json3
-rw-r--r--FrontEnd/src/locales/zh/translation.json2
-rw-r--r--FrontEnd/src/pages/setting/index.tsx279
-rw-r--r--FrontEnd/src/views/common/Card.tsx2
-rw-r--r--FrontEnd/src/views/common/button/Button.tsx4
-rw-r--r--FrontEnd/src/views/common/button/FlatButton.tsx4
-rw-r--r--FrontEnd/src/views/common/common.ts2
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 = [