diff options
Diffstat (limited to 'FrontEnd')
-rw-r--r-- | FrontEnd/src/App.tsx | 8 | ||||
-rw-r--r-- | FrontEnd/src/index.css | 1 | ||||
-rw-r--r-- | FrontEnd/src/pages/about/index.css | 1 | ||||
-rw-r--r-- | FrontEnd/src/pages/about/index.tsx | 5 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 6 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx | 2 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/ChangePasswordDialog.tsx | 2 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/index.css | 54 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/index.tsx | 171 | ||||
-rw-r--r-- | FrontEnd/src/views/common/Card.css | 8 | ||||
-rw-r--r-- | FrontEnd/src/views/common/Card.tsx | 8 | ||||
-rw-r--r-- | FrontEnd/src/views/common/Page.tsx | 15 | ||||
-rw-r--r-- | FrontEnd/src/views/common/index.css | 8 | ||||
-rw-r--r-- | FrontEnd/src/views/common/theme.css | 5 | ||||
-rw-r--r-- | FrontEnd/src/views/settings/ChangeAvatarDialog.tsx | 354 | ||||
-rw-r--r-- | FrontEnd/src/views/settings/ChangeNicknameDialog.tsx | 34 | ||||
-rw-r--r-- | FrontEnd/src/views/settings/ChangePasswordDialog.tsx | 69 | ||||
-rw-r--r-- | FrontEnd/src/views/settings/index.css | 31 | ||||
-rw-r--r-- | FrontEnd/src/views/settings/index.tsx | 338 |
19 files changed, 182 insertions, 938 deletions
diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx index 07a8780f..92fe0652 100644 --- a/FrontEnd/src/App.tsx +++ b/FrontEnd/src/App.tsx @@ -4,11 +4,11 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import AppBar from "./views/common/AppBar"; import NotFoundPage from "./pages/404"; import LoadingPage from "./views/common/LoadingPage"; -import About from "./pages/about"; +import AboutPage from "./pages/about"; +import SettingPage from "./pages/setting"; import Center from "./views/center"; import Login from "./views/login"; import Register from "./views/register"; -import Settings from "./views/settings"; import TimelinePage from "./views/timeline"; import Search from "./views/search"; import Admin from "./views/admin"; @@ -24,8 +24,8 @@ export default function App() { <Route path="center" element={<Center />} /> <Route path="login" element={<Login />} /> <Route path="register" element={<Register />} /> - <Route path="settings" element={<Settings />} /> - <Route path="about" element={<About />} /> + <Route path="settings" element={<SettingPage />} /> + <Route path="about" element={<AboutPage />} /> <Route path="search" element={<Search />} /> <Route path="admin/*" element={<Admin />} /> <Route path=":owner" element={<TimelinePage />} /> diff --git a/FrontEnd/src/index.css b/FrontEnd/src/index.css index 3478db05..49791c23 100644 --- a/FrontEnd/src/index.css +++ b/FrontEnd/src/index.css @@ -1,4 +1,3 @@ -@import "npm:bootstrap/dist/css/bootstrap-grid.css";
@import "npm:bootstrap-icons/font/bootstrap-icons.css";
@import "./views/common/index.css";
diff --git a/FrontEnd/src/pages/about/index.css b/FrontEnd/src/pages/about/index.css index 487f4a0a..1ce7a7c8 100644 --- a/FrontEnd/src/pages/about/index.css +++ b/FrontEnd/src/pages/about/index.css @@ -1,5 +1,4 @@ .about-page { - padding: 1em 2em; line-height: 1.5; } diff --git a/FrontEnd/src/pages/about/index.tsx b/FrontEnd/src/pages/about/index.tsx index afd4de34..acec1735 100644 --- a/FrontEnd/src/pages/about/index.tsx +++ b/FrontEnd/src/pages/about/index.tsx @@ -1,6 +1,7 @@ import "./index.css"; import { useC } from "@/common"; +import Page from "@/views/common/Page"; interface Credit { name: string; @@ -52,7 +53,7 @@ export default function AboutPage() { const c = useC(); return ( - <div className="about-page"> + <Page className="about-page"> <h2>{c("about.credits.title")}</h2> <p>{c("about.credits.content")}</p> <h3>{c("about.credits.frontend")}</h3> @@ -81,6 +82,6 @@ export default function AboutPage() { })} <li>...</li> </ul> - </div> + </Page> ); } diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index 44bd2c68..b2a4e2a8 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -9,9 +9,9 @@ import { useUser } from "@/services/user"; import { getHttpUserClient } from "@/http/user"; -import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; -import Button from "../common/button/Button"; -import Dialog from "../common/dialog/Dialog"; +import ImageCropper, { Clip, applyClipToImage } from "@/views/common/ImageCropper"; +import Button from "@/views/common/button/Button"; +import Dialog from "@/views/common/dialog/Dialog"; export interface ChangeAvatarDialogProps { open: boolean; diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx index 7ba12de8..11c86222 100644 --- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx @@ -2,7 +2,7 @@ import { getHttpUserClient } from "@/http/user"; import { useUser } from "@/services/user"; import * as React from "react"; -import OperationDialog from "../common/dialog/OperationDialog"; +import OperationDialog from "@/views/common/dialog/OperationDialog"; export interface ChangeNicknameDialogProps { open: boolean; diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index a34ca4a7..a523b454 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom"; import { userService } from "@/services/user"; -import OperationDialog from "../common/dialog/OperationDialog"; +import OperationDialog from "@/views/common/dialog/OperationDialog"; export interface ChangePasswordDialogProps { open: boolean; diff --git a/FrontEnd/src/pages/setting/index.css b/FrontEnd/src/pages/setting/index.css index ccf7a97a..af5ccf20 100644 --- a/FrontEnd/src/pages/setting/index.css +++ b/FrontEnd/src/pages/setting/index.css @@ -1,3 +1,5 @@ +/* TODO: Make item prettier. */
+
.change-avatar-cropper-row {
max-height: 400px;
}
@@ -8,20 +10,56 @@ max-height: 400px;
}
-.settings-item {
+.setting-section {
+ padding: 1em 0;
+ margin: 1em 0;
+}
+
+.setting-section-title {
+ padding: 0 1em;
+}
+
+.setting-section-item-area {
+ margin-top: 1em;
+ border-top: 1px solid var(--cru-key-container-color);
+}
+
+.setting-item-container {
padding: 0.5em 1em;
- transition: background 0.3s;
- border-bottom: 1px solid #e9ecef;
+ transition: background-color 0.3s;
+ color: var(--cru-surface-on-color);
+ background-color: var(--cru-surface-color);
+ border-bottom: 1px solid var(--cru-key-container-color);
+ display: flex;
align-items: center;
}
-.settings-item.first {
- border-top: 1px solid #e9ecef;
+
+.setting-item-container:hover {
+ background-color: var(--cru-key-container-1-color);
+ border-bottom-color: var(--cru-key-container-1-color);
+}
+
+.setting-item-label-sub {
+ color: var(--cru-secondary-text-color);
}
-.settings-item.clickable {
+
+.setting-item-container.setting-type-button {
cursor: pointer;
}
-.settings-item:hover {
- background: #dee2e6;
+
+.setting-item-container.setting-type-button.danger {
+ color: var(--cru-danger-color);
+}
+
+
+@media (max-width: 576) {
+ .setting-item-container.setting-type-select {
+ flex-direction: column;
+ }
+
+ .setting-item-container.setting-type-select .setting-item-value-area {
+ margin-top: 1em;
+ }
}
.register-code {
diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 4e28585e..5d6dcbc0 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -11,11 +11,11 @@ import classNames from "classnames"; import { useC, Text } from "@/common"; import { useUser, userService } from "@/services/user"; import { getHttpUserClient } from "@/http/user"; -import { TimelineVisibility } from "@/http/timeline"; import ConfirmDialog from "@/views/common/dialog/ConfirmDialog"; import Card from "@/views/common/Card"; import Spinner from "@/views/common/Spinner"; +import Page from "@/views/common/Page"; import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; @@ -24,9 +24,9 @@ import "./index.css"; import { pushAlert } from "@/services/alert"; interface SettingSectionProps - extends Omit<ComponentPropsWithoutRef<"div">, "title"> { + extends Omit<ComponentPropsWithoutRef<typeof Card>, "title"> { title: Text; - children: ReactNode; + children?: ReactNode; } function SettingSection({ @@ -40,7 +40,7 @@ function SettingSection({ return ( <Card className={classNames(className, "setting-section")} {...otherProps}> <h2 className="setting-section-title">{c(title)}</h2> - {children} + <div className="setting-section-item-area">{children}</div> </Card> ); } @@ -115,6 +115,7 @@ function SelectSettingsItem({ <Spinner /> ) : ( <select + className="select-setting-item-select" value={value} onChange={(e) => { onSelect(e.target.value); @@ -131,16 +132,14 @@ function SelectSettingsItem({ ); } -function RegisterCodeSettingItem({ - openRenewDialog, -}: { - openRenewDialog: () => void; -}) { +function RegisterCodeSettingItem() { const user = useUser(); // undefined: loading const [registerCode, setRegisterCode] = useState<undefined | null | string>(); + const [dialogOpen, setDialogOpen] = useState(false); + useEffect(() => { setRegisterCode(undefined); }, [user]); @@ -159,7 +158,7 @@ function RegisterCodeSettingItem({ <SettingItemContainer title="settings.myRegisterCode" description="settings.myRegisterCodeDesc" - onClick={openRenewDialog} + onClick={() => setDialogOpen(true)} > {registerCode === undefined ? ( <Spinner /> @@ -181,13 +180,59 @@ function RegisterCodeSettingItem({ {registerCode} </code> )} + + <ConfirmDialog + title="settings.renewRegisterCode" + body="settings.renewRegisterCodeDesc" + onClose={() => setDialogOpen(false)} + open={dialogOpen} + onConfirm={() => { + if (user == null) throw new Error(); + void getHttpUserClient() + .renewRegisterCode(user.username) + .then(() => { + setRegisterCode(undefined); + }); + }} + /> </SettingItemContainer> ); } -export default function SettingsPage() { - const c = useC(); +function LanguageChangeSettingItem() { const { i18n } = useTranslation(); + + const language = i18n.language.slice(0, 2); + + return ( + <SelectSettingsItem + title="settings.languagePrimary" + description="settings.languageSecondary" + options={[ + { + value: "zh", + label: { + type: "custom", + value: "中文", + }, + }, + { + value: "en", + label: { + type: "custom", + value: "English", + }, + }, + ]} + value={language} + onSelect={(value) => { + void i18n.changeLanguage(value); + }} + /> + ); +} + +export default function SettingPage() { const user = useUser(); const navigate = useNavigate(); @@ -204,66 +249,36 @@ export default function SettingsPage() { return () => setDialog(name); } - const language = i18n.language.slice(0, 2); - return ( - <> - <div className="container"> - {user ? ( - <SettingSection title="settings.subheader.account"> - <RegisterCodeSettingItem - openRenewDialog={dialogOpener("renew-register-code")} - /> - <ButtonSettingItem - title="settings.changeAvatar" - onClick={dialogOpener("change-avatar")} - /> - <ButtonSettingItem - title="settings.changeNickname" - onClick={dialogOpener("change-nickname")} - /> - <ButtonSettingItem - title="settings.changePassword" - onClick={dialogOpener("change-password")} - danger - /> - <ButtonSettingItem - title="settings.logout" - onClick={dialogOpener("logout")} - danger - /> - </SettingSection> - ) : null} - <SettingSection title="settings.subheaders.customization"> - <SelectSettingsItem - title="settings.languagePrimary" - subtext="settings.languageSecondary" - options={[ - { - value: "zh", - label: { - type: "custom", - value: "中文", - }, - }, - { - value: "en", - label: { - type: "custom", - value: "English", - }, - }, - ]} - value={language} - onSelect={(value) => { - void i18n.changeLanguage(value); - }} - first + <Page noTopPadding> + {user ? ( + <SettingSection title="settings.subheader.account"> + <RegisterCodeSettingItem /> + <ButtonSettingItem + title="settings.changeAvatar" + onClick={dialogOpener("change-avatar")} + /> + <ButtonSettingItem + title="settings.changeNickname" + onClick={dialogOpener("change-nickname")} + /> + <ButtonSettingItem + title="settings.changePassword" + onClick={dialogOpener("change-password")} + danger + /> + <ButtonSettingItem + title="settings.logout" + onClick={dialogOpener("logout")} + danger /> </SettingSection> - </div> + ) : null} + <SettingSection title="settings.subheader.customization"> + <LanguageChangeSettingItem /> + </SettingSection> <ChangePasswordDialog - open={dialog === "changepassword"} + open={dialog === "change-password"} close={() => setDialog(null)} /> <ConfirmDialog @@ -277,28 +292,14 @@ export default function SettingsPage() { }); }} /> - <ConfirmDialog - title="settings.renewRegisterCode" - body="settings.renewRegisterCodeDesc" - onClose={() => setDialog(null)} - open={dialog === "renewregistercode"} - onConfirm={() => { - if (user == null) throw new UiLogicError(); - void getHttpUserClient() - .renewRegisterCode(user.username) - .then(() => { - setRegisterCode(undefined); - }); - }} - /> <ChangeAvatarDialog - open={dialog === "changeavatar"} + open={dialog === "change-avatar"} close={() => setDialog(null)} /> <ChangeNicknameDialog - open={dialog === "changenickname"} + open={dialog === "change-nickname"} close={() => setDialog(null)} /> - </> + </Page> ); } diff --git a/FrontEnd/src/views/common/Card.css b/FrontEnd/src/views/common/Card.css index 98cb4cdd..5b3dbbe9 100644 --- a/FrontEnd/src/views/common/Card.css +++ b/FrontEnd/src/views/common/Card.css @@ -1,9 +1,11 @@ .cru-card {
+ border: solid 1px;
border-radius: var(--cru-card-border-radius);
- background-color: var(--cru-primary-container-color);
+ background-color: var(--cru-key-container-color);
+ border-color: var(--cru-key-container-color);
transition: all 0.3s;
}
.cru-card:hover {
- border-color: var(--cru-primary-1-color);
-}
+ border-color: var(--cru-key-1-color);
+}
\ No newline at end of file diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx index 5ff89b61..35e605af 100644 --- a/FrontEnd/src/views/common/Card.tsx +++ b/FrontEnd/src/views/common/Card.tsx @@ -1,22 +1,26 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; +import { ThemeColor } from "./common"; import "./Card.css"; interface CardProps extends ComponentPropsWithoutRef<"div"> { - containerRef?: Ref<HTMLDivElement> | null; + containerRef?: Ref<HTMLDivElement>; + color?: ThemeColor; } export default function Card({ + color, className, children, containerRef, ...otherProps }: CardProps) { + color = color ?? "primary"; return ( <div ref={containerRef} - className={classNames("cru-card", className)} + className={classNames("cru-card", `cru-${color}`, className)} {...otherProps} > {children} diff --git a/FrontEnd/src/views/common/Page.tsx b/FrontEnd/src/views/common/Page.tsx new file mode 100644 index 00000000..86fdb2f5 --- /dev/null +++ b/FrontEnd/src/views/common/Page.tsx @@ -0,0 +1,15 @@ +import { ComponentPropsWithoutRef, Ref } from "react"; +import classNames from "classnames"; + +interface PageProps extends ComponentPropsWithoutRef<"div"> { + noTopPadding?: boolean; + pageRef?: Ref<HTMLDivElement>; +} + +export default function Page({ noTopPadding, pageRef, className, children }: PageProps) { + return ( + <div ref={pageRef} className={classNames(className, "cru-page", noTopPadding && "cru-page-no-top-padding")}> + {children} + </div> + ); +} diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css index eb82c4bf..1f9b5086 100644 --- a/FrontEnd/src/views/common/index.css +++ b/FrontEnd/src/views/common/index.css @@ -13,6 +13,14 @@ body { line-height: 1.2;
}
+.cru-page {
+ padding: var(--cru-page-padding);
+}
+
+.cru-page-no-top-padding {
+ padding-top: 0;
+}
+
.cru-text-center {
text-align: center;
}
diff --git a/FrontEnd/src/views/common/theme.css b/FrontEnd/src/views/common/theme.css index 3ad45996..6478ed7a 100644 --- a/FrontEnd/src/views/common/theme.css +++ b/FrontEnd/src/views/common/theme.css @@ -2,5 +2,8 @@ :root { --cru-default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --cru-page-padding: 1em 2em; --cru-card-border-radius: 4px; -}
\ No newline at end of file + + --cru-secondary-text-color: var(--cru-surface-on-color); +} diff --git a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx deleted file mode 100644 index 44bd2c68..00000000 --- a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx +++ /dev/null @@ -1,354 +0,0 @@ -import { useState, useEffect } from "react"; -import * as React from "react"; -import { useTranslation } from "react-i18next"; -import { AxiosError } from "axios"; - -import { convertI18nText, I18nText, UiLogicError } from "@/common"; - -import { useUser } from "@/services/user"; - -import { getHttpUserClient } from "@/http/user"; - -import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; -import Button from "../common/button/Button"; -import Dialog from "../common/dialog/Dialog"; - -export interface ChangeAvatarDialogProps { - open: boolean; - close: () => void; -} - -const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { - const { t } = useTranslation(); - - const user = useUser(); - - const [file, setFile] = React.useState<File | null>(null); - const [fileUrl, setFileUrl] = React.useState<string | null>(null); - const [clip, setClip] = React.useState<Clip | null>(null); - const [cropImgElement, setCropImgElement] = - React.useState<HTMLImageElement | null>(null); - const [resultBlob, setResultBlob] = React.useState<Blob | null>(null); - const [resultUrl, setResultUrl] = React.useState<string | null>(null); - - const [state, setState] = React.useState< - | "select" - | "crop" - | "processcrop" - | "preview" - | "uploading" - | "success" - | "error" - >("select"); - - const [message, setMessage] = useState<I18nText>( - "settings.dialogChangeAvatar.prompt.select" - ); - - const trueMessage = convertI18nText(message, t); - - 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<HTMLInputElement>): 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(); - } - - if (user == null) { - throw new UiLogicError(); - } - - setState("uploading"); - getHttpUserClient() - .putAvatar(user.username, resultBlob) - .then( - () => { - setState("success"); - }, - (e: unknown) => { - setState("error"); - setMessage({ type: "custom", value: (e as AxiosError).message }); - } - ); - }, [user, resultBlob]); - - const createPreviewRow = (): React.ReactElement => { - if (resultUrl == null) { - throw new UiLogicError(); - } - return ( - <div className="row justify-content-center"> - <div className="col col-auto"> - <img - className="change-avatar-img" - src={resultUrl} - alt={t("settings.dialogChangeAvatar.previewImgAlt") ?? undefined} - /> - </div> - </div> - ); - }; - - return ( - <Dialog open={props.open} onClose={close}> - <h3 className="cru-color-primary"> - {t("settings.dialogChangeAvatar.title")} - </h3> - <hr /> - {(() => { - if (state === "select") { - return ( - <> - <div className="container"> - <div className="row"> - {t("settings.dialogChangeAvatar.prompt.select")} - </div> - <div className="row"> - <input - className="px-0" - type="file" - accept="image/*" - onChange={onSelectFile} - /> - </div> - </div> - <hr /> - <div className="cru-dialog-bottom-area"> - <Button - text="operationDialog.cancel" - color="secondary" - onClick={close} - /> - </div> - </> - ); - } else if (state === "crop") { - if (fileUrl == null) { - throw new UiLogicError(); - } - return ( - <> - <div className="container"> - <div className="row justify-content-center"> - <ImageCropper - clip={clip} - onChange={setClip} - imageUrl={fileUrl} - imageElementCallback={setCropImgElement} - /> - </div> - <div className="row"> - {t("settings.dialogChangeAvatar.prompt.crop")} - </div> - </div> - <hr /> - <div className="cru-dialog-bottom-area"> - <Button - text="operationDialog.cancel" - color="secondary" - outline - onClick={close} - /> - <Button - text="operationDialog.previousStep" - color="secondary" - outline - onClick={onCropPrevious} - /> - <Button - text="operationDialog.nextStep" - color="primary" - onClick={onCropNext} - disabled={ - cropImgElement == null || clip == null || clip.width === 0 - } - /> - </div> - </> - ); - } else if (state === "processcrop") { - return ( - <> - <div className="container"> - <div className="row"> - {t("settings.dialogChangeAvatar.prompt.processingCrop")} - </div> - </div> - <hr /> - <div className="cru-dialog-bottom-area"> - <Button - text="operationDialog.cancel" - color="secondary" - onClick={close} - outline - /> - <Button - text="operationDialog.previousStep" - color="secondary" - onClick={onPreviewPrevious} - outline - /> - </div> - </> - ); - } else if (state === "preview") { - return ( - <> - <div className="container"> - {createPreviewRow()} - <div className="row"> - {t("settings.dialogChangeAvatar.prompt.preview")} - </div> - </div> - <hr /> - <div className="cru-dialog-bottom-area"> - <Button - text="operationDialog.cancel" - color="secondary" - outline - onClick={close} - /> - <Button - text="operationDialog.previousStep" - color="secondary" - outline - onClick={onPreviewPrevious} - /> - <Button - text="settings.dialogChangeAvatar.upload" - color="primary" - onClick={upload} - /> - </div> - </> - ); - } else if (state === "uploading") { - return ( - <> - <div className="container"> - {createPreviewRow()} - <div className="row"> - {t("settings.dialogChangeAvatar.prompt.uploading")} - </div> - </div> - </> - ); - } else if (state === "success") { - return ( - <> - <div className="container"> - <div className="row p-4 text-success"> - {t("operationDialog.success")} - </div> - </div> - <hr /> - <div className="cru-dialog-bottom-area"> - <Button - text="operationDialog.ok" - color="success" - onClick={close} - /> - </div> - </> - ); - } else { - return ( - <> - <div className="container"> - {createPreviewRow()} - <div className="row text-danger">{trueMessage}</div> - </div> - <hr /> - <div> - <Button - text="operationDialog.cancel" - color="secondary" - onClick={close} - /> - <Button - text="operationDialog.retry" - color="primary" - onClick={upload} - /> - </div> - </> - ); - } - })()} - </Dialog> - ); -}; - -export default ChangeAvatarDialog; diff --git a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx deleted file mode 100644 index 7ba12de8..00000000 --- a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { getHttpUserClient } from "@/http/user"; -import { useUser } from "@/services/user"; -import * as React from "react"; - -import OperationDialog from "../common/dialog/OperationDialog"; - -export interface ChangeNicknameDialogProps { - open: boolean; - close: () => void; -} - -const ChangeNicknameDialog: React.FC<ChangeNicknameDialogProps> = (props) => { - const user = useUser(); - - if (user == null) return null; - - return ( - <OperationDialog - open={props.open} - title="settings.dialogChangeNickname.title" - inputScheme={[ - { type: "text", label: "settings.dialogChangeNickname.inputLabel" }, - ]} - onProcess={([newNickname]) => { - return getHttpUserClient().patch(user.username, { - nickname: newNickname, - }); - }} - onClose={props.close} - /> - ); -}; - -export default ChangeNicknameDialog; diff --git a/FrontEnd/src/views/settings/ChangePasswordDialog.tsx b/FrontEnd/src/views/settings/ChangePasswordDialog.tsx deleted file mode 100644 index a34ca4a7..00000000 --- a/FrontEnd/src/views/settings/ChangePasswordDialog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useState } from "react"; -import * as React from "react"; -import { useNavigate } from "react-router-dom"; - -import { userService } from "@/services/user"; - -import OperationDialog from "../common/dialog/OperationDialog"; - -export interface ChangePasswordDialogProps { - open: boolean; - close: () => void; -} - -const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => { - const navigate = useNavigate(); - - const [redirect, setRedirect] = useState<boolean>(false); - - return ( - <OperationDialog - open={props.open} - title="settings.dialogChangePassword.title" - themeColor="danger" - inputPrompt="settings.dialogChangePassword.prompt" - inputScheme={[ - { - type: "text", - label: "settings.dialogChangePassword.inputOldPassword", - password: true, - }, - { - type: "text", - label: "settings.dialogChangePassword.inputNewPassword", - password: true, - }, - { - type: "text", - label: "settings.dialogChangePassword.inputRetypeNewPassword", - password: true, - }, - ]} - inputValidator={([oldPassword, newPassword, retypedNewPassword]) => { - const result: Record<number, string> = {}; - 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); - }} - onClose={() => { - props.close(); - if (redirect) { - navigate("/login"); - } - }} - /> - ); -}; - -export default ChangePasswordDialog; diff --git a/FrontEnd/src/views/settings/index.css b/FrontEnd/src/views/settings/index.css deleted file mode 100644 index ccf7a97a..00000000 --- a/FrontEnd/src/views/settings/index.css +++ /dev/null @@ -1,31 +0,0 @@ -.change-avatar-cropper-row {
- max-height: 400px;
-}
-
-.change-avatar-img {
- min-width: 50%;
- max-width: 100%;
- max-height: 400px;
-}
-
-.settings-item {
- padding: 0.5em 1em;
- transition: background 0.3s;
- border-bottom: 1px solid #e9ecef;
- align-items: center;
-}
-.settings-item.first {
- border-top: 1px solid #e9ecef;
-}
-.settings-item.clickable {
- cursor: pointer;
-}
-.settings-item:hover {
- background: #dee2e6;
-}
-
-.register-code {
- border: 1px solid black;
- border-radius: 3px;
- padding: 0.2em;
-}
\ No newline at end of file diff --git a/FrontEnd/src/views/settings/index.tsx b/FrontEnd/src/views/settings/index.tsx deleted file mode 100644 index 6647826f..00000000 --- a/FrontEnd/src/views/settings/index.tsx +++ /dev/null @@ -1,338 +0,0 @@ -import { useState } from "react"; -import * as React from "react"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import classNames from "classnames"; - -import { convertI18nText, I18nText, UiLogicError } 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 ChangePasswordDialog from "./ChangePasswordDialog"; -import ChangeAvatarDialog from "./ChangeAvatarDialog"; -import ChangeNicknameDialog from "./ChangeNicknameDialog"; - -import "./index.css"; -import { pushAlert } from "@/services/alert"; - -interface SettingSectionProps { - title: I18nText; - children: React.ReactNode; -} - -const SettingSection: React.FC<SettingSectionProps> = ({ title, children }) => { - const { t } = useTranslation(); - - return ( - <Card className="my-3 py-3"> - <h3 className="px-3 mb-3 cru-color-primary"> - {convertI18nText(title, t)} - </h3> - {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; -} - -function SettingItemContainer({ - title, - subtext, - first, - danger, - children, - style, - className, - onClick, -}: SettingItemContainerProps): JSX.Element { - const { t } = useTranslation(); - - return ( - <div - style={style} - className={classNames( - "row settings-item mx-0", - first && "first", - onClick && "clickable", - className, - )} - onClick={onClick} - > - <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> - <div className="col col-auto">{children}</div> - </div> - ); -} - -type ButtonSettingItemProps = SettingItemContainerWithoutChildrenProps; - -const ButtonSettingItem: React.FC<ButtonSettingItemProps> = ({ ...props }) => { - return <SettingItemContainer {...props} />; -}; - -interface SelectSettingItemProps - extends SettingItemContainerWithoutChildrenProps { - options: { - value: string; - label: I18nText; - }[]; - value?: string; - onSelect: (value: string) => void; -} - -const SelectSettingsItem: React.FC<SelectSettingItemProps> = ({ - options, - value, - onSelect, - ...props -}) => { - const { t } = useTranslation(); - - return ( - <SettingItemContainer {...props}> - {value == null ? ( - <Spinner /> - ) : ( - <select - value={value} - onChange={(e) => { - onSelect(e.target.value); - }} - > - {options.map(({ value, label }) => ( - <option key={value} value={value}> - {convertI18nText(label, t)} - </option> - ))} - </select> - )} - </SettingItemContainer> - ); -}; - -const SettingsPage: React.FC = () => { - const { i18n } = useTranslation(); - 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]); - - React.useEffect(() => { - setRegisterCode(undefined); - }, [user]); - - React.useEffect(() => { - if (user != null && registerCode === undefined) { - void getHttpUserClient() - .getRegisterCode(user.username) - .then((code) => { - setRegisterCode(code.registerCode ?? null); - }); - } - }, [user, registerCode]); - - 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> - <ButtonSettingItem - title="settings.changeAvatar" - onClick={() => setDialog("changeavatar")} - first - /> - <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); - }); - }} - /> - <ButtonSettingItem - title="settings.changePassword" - onClick={() => setDialog("changepassword")} - danger - /> - <ButtonSettingItem - title="settings.logout" - onClick={() => { - setDialog("logout"); - }} - danger - /> - </SettingSection> - ) : null} - <SettingSection title="settings.subheaders.customization"> - <SelectSettingsItem - title="settings.languagePrimary" - subtext="settings.languageSecondary" - options={[ - { - value: "zh", - label: { - type: "custom", - value: "中文", - }, - }, - { - value: "en", - label: { - type: "custom", - value: "English", - }, - }, - ]} - value={language} - onSelect={(value) => { - void i18n.changeLanguage(value); - }} - first - /> - </SettingSection> - </div> - <ChangePasswordDialog - open={dialog === "changepassword"} - close={() => setDialog(null)} - /> - <ConfirmDialog - title="settings.dialogConfirmLogout.title" - body="settings.dialogConfirmLogout.prompt" - onClose={() => setDialog(null)} - open={dialog === "logout"} - onConfirm={() => { - void userService.logout().then(() => { - navigate("/"); - }); - }} - /> - <ConfirmDialog - title="settings.renewRegisterCode" - body="settings.renewRegisterCodeDesc" - onClose={() => setDialog(null)} - open={dialog === "renewregistercode"} - onConfirm={() => { - if (user == null) throw new UiLogicError(); - void getHttpUserClient() - .renewRegisterCode(user.username) - .then(() => { - setRegisterCode(undefined); - }); - }} - /> - <ChangeAvatarDialog - open={dialog === "changeavatar"} - close={() => setDialog(null)} - /> - <ChangeNicknameDialog - open={dialog === "changenickname"} - close={() => setDialog(null)} - /> - </> - ); -}; - -export default SettingsPage; |