aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/App.tsx8
-rw-r--r--FrontEnd/src/index.css1
-rw-r--r--FrontEnd/src/pages/about/index.css1
-rw-r--r--FrontEnd/src/pages/about/index.tsx5
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx6
-rw-r--r--FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx2
-rw-r--r--FrontEnd/src/pages/setting/ChangePasswordDialog.tsx2
-rw-r--r--FrontEnd/src/pages/setting/index.css54
-rw-r--r--FrontEnd/src/pages/setting/index.tsx171
-rw-r--r--FrontEnd/src/views/common/Card.css8
-rw-r--r--FrontEnd/src/views/common/Card.tsx8
-rw-r--r--FrontEnd/src/views/common/Page.tsx15
-rw-r--r--FrontEnd/src/views/common/index.css8
-rw-r--r--FrontEnd/src/views/common/theme.css5
-rw-r--r--FrontEnd/src/views/settings/ChangeAvatarDialog.tsx354
-rw-r--r--FrontEnd/src/views/settings/ChangeNicknameDialog.tsx34
-rw-r--r--FrontEnd/src/views/settings/ChangePasswordDialog.tsx69
-rw-r--r--FrontEnd/src/views/settings/index.css31
-rw-r--r--FrontEnd/src/views/settings/index.tsx338
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;