aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app/views/settings
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/app/views/settings')
-rw-r--r--FrontEnd/src/app/views/settings/ChangeAvatarDialog.tsx305
-rw-r--r--FrontEnd/src/app/views/settings/ChangeNicknameDialog.tsx32
-rw-r--r--FrontEnd/src/app/views/settings/ChangePasswordDialog.tsx68
-rw-r--r--FrontEnd/src/app/views/settings/index.tsx138
-rw-r--r--FrontEnd/src/app/views/settings/settings.sass14
5 files changed, 0 insertions, 557 deletions
diff --git a/FrontEnd/src/app/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/app/views/settings/ChangeAvatarDialog.tsx
deleted file mode 100644
index c4f6f492..00000000
--- a/FrontEnd/src/app/views/settings/ChangeAvatarDialog.tsx
+++ /dev/null
@@ -1,305 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useTranslation } from "react-i18next";
-import { AxiosError } from "axios";
-import { Modal, Row, Button } from "react-bootstrap";
-
-import { UiLogicError } from "@/common";
-
-import { useUserLoggedIn } from "@/services/user";
-
-import { getHttpUserClient } from "@/http/user";
-
-import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper";
-
-export interface ChangeAvatarDialogProps {
- open: boolean;
- close: () => void;
-}
-
-const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => {
- const { t } = useTranslation();
-
- const user = useUserLoggedIn();
-
- 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<
- string | { type: "custom"; text: string } | null
- >("settings.dialogChangeAvatar.prompt.select");
-
- const trueMessage =
- message == null
- ? null
- : typeof message === "string"
- ? t(message)
- : message.text;
-
- const closeDialog = props.close;
-
- const close = React.useCallback((): void => {
- if (!(state === "uploading")) {
- closeDialog();
- }
- }, [state, closeDialog]);
-
- useEffect(() => {
- if (file != null) {
- const url = URL.createObjectURL(file);
- setClip(null);
- setFileUrl(url);
- setState("crop");
- return () => {
- URL.revokeObjectURL(url);
- };
- } else {
- setFileUrl(null);
- setState("select");
- }
- }, [file]);
-
- React.useEffect(() => {
- if (resultBlob != null) {
- const url = URL.createObjectURL(resultBlob);
- setResultUrl(url);
- setState("preview");
- return () => {
- URL.revokeObjectURL(url);
- };
- } else {
- setResultUrl(null);
- }
- }, [resultBlob]);
-
- const onSelectFile = React.useCallback(
- (e: React.ChangeEvent<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();
- }
-
- setState("uploading");
- getHttpUserClient()
- .putAvatar(user.username, resultBlob)
- .then(
- () => {
- setState("success");
- },
- (e: unknown) => {
- setState("error");
- setMessage({ type: "custom", text: (e as AxiosError).message });
- }
- );
- }, [user.username, resultBlob]);
-
- const createPreviewRow = (): React.ReactElement => {
- if (resultUrl == null) {
- throw new UiLogicError();
- }
- return (
- <Row className="justify-content-center">
- <img
- className="change-avatar-img"
- src={resultUrl}
- alt={t("settings.dialogChangeAvatar.previewImgAlt")}
- />
- </Row>
- );
- };
-
- return (
- <Modal show={props.open} onHide={close}>
- <Modal.Header>
- <Modal.Title> {t("settings.dialogChangeAvatar.title")}</Modal.Title>
- </Modal.Header>
- {(() => {
- if (state === "select") {
- return (
- <>
- <Modal.Body className="container">
- <Row>{t("settings.dialogChangeAvatar.prompt.select")}</Row>
- <Row>
- <input type="file" accept="image/*" onChange={onSelectFile} />
- </Row>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={close}>
- {t("operationDialog.cancel")}
- </Button>
- </Modal.Footer>
- </>
- );
- } else if (state === "crop") {
- if (fileUrl == null) {
- throw new UiLogicError();
- }
- return (
- <>
- <Modal.Body className="container">
- <Row className="justify-content-center">
- <ImageCropper
- clip={clip}
- onChange={setClip}
- imageUrl={fileUrl}
- imageElementCallback={setCropImgElement}
- />
- </Row>
- <Row>{t("settings.dialogChangeAvatar.prompt.crop")}</Row>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={close}>
- {t("operationDialog.cancel")}
- </Button>
- <Button variant="secondary" onClick={onCropPrevious}>
- {t("operationDialog.previousStep")}
- </Button>
- <Button
- color="primary"
- onClick={onCropNext}
- disabled={
- cropImgElement == null || clip == null || clip.width === 0
- }
- >
- {t("operationDialog.nextStep")}
- </Button>
- </Modal.Footer>
- </>
- );
- } else if (state === "processcrop") {
- return (
- <>
- <Modal.Body className="container">
- <Row>
- {t("settings.dialogChangeAvatar.prompt.processingCrop")}
- </Row>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={close}>
- {t("operationDialog.cancel")}
- </Button>
- <Button variant="secondary" onClick={onPreviewPrevious}>
- {t("operationDialog.previousStep")}
- </Button>
- </Modal.Footer>
- </>
- );
- } else if (state === "preview") {
- return (
- <>
- <Modal.Body className="container">
- {createPreviewRow()}
- <Row>{t("settings.dialogChangeAvatar.prompt.preview")}</Row>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={close}>
- {t("operationDialog.cancel")}
- </Button>
- <Button variant="secondary" onClick={onPreviewPrevious}>
- {t("operationDialog.previousStep")}
- </Button>
- <Button variant="primary" onClick={upload}>
- {t("settings.dialogChangeAvatar.upload")}
- </Button>
- </Modal.Footer>
- </>
- );
- } else if (state === "uploading") {
- return (
- <>
- <Modal.Body className="container">
- {createPreviewRow()}
- <Row>{t("settings.dialogChangeAvatar.prompt.uploading")}</Row>
- </Modal.Body>
- <Modal.Footer></Modal.Footer>
- </>
- );
- } else if (state === "success") {
- return (
- <>
- <Modal.Body className="container">
- <Row className="p-4 text-success">
- {t("operationDialog.success")}
- </Row>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="success" onClick={close}>
- {t("operationDialog.ok")}
- </Button>
- </Modal.Footer>
- </>
- );
- } else {
- return (
- <>
- <Modal.Body className="container">
- {createPreviewRow()}
- <Row className="text-danger">{trueMessage}</Row>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={close}>
- {t("operationDialog.cancel")}
- </Button>
- <Button variant="primary" onClick={upload}>
- {t("operationDialog.retry")}
- </Button>
- </Modal.Footer>
- </>
- );
- }
- })()}
- </Modal>
- );
-};
-
-export default ChangeAvatarDialog;
diff --git a/FrontEnd/src/app/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/app/views/settings/ChangeNicknameDialog.tsx
deleted file mode 100644
index 4b44cdd6..00000000
--- a/FrontEnd/src/app/views/settings/ChangeNicknameDialog.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { getHttpUserClient } from "@/http/user";
-import { useUserLoggedIn } from "@/services/user";
-import React from "react";
-
-import OperationDialog from "../common/OperationDialog";
-
-export interface ChangeNicknameDialogProps {
- open: boolean;
- close: () => void;
-}
-
-const ChangeNicknameDialog: React.FC<ChangeNicknameDialogProps> = (props) => {
- const user = useUserLoggedIn();
-
- 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,
- });
- }}
- close={props.close}
- />
- );
-};
-
-export default ChangeNicknameDialog;
diff --git a/FrontEnd/src/app/views/settings/ChangePasswordDialog.tsx b/FrontEnd/src/app/views/settings/ChangePasswordDialog.tsx
deleted file mode 100644
index 21eeeb09..00000000
--- a/FrontEnd/src/app/views/settings/ChangePasswordDialog.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React, { useState } from "react";
-import { useHistory } from "react-router";
-
-import { userService } from "@/services/user";
-
-import OperationDialog from "../common/OperationDialog";
-
-export interface ChangePasswordDialogProps {
- open: boolean;
- close: () => void;
-}
-
-const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => {
- const history = useHistory();
-
- 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);
- }}
- close={() => {
- props.close();
- if (redirect) {
- history.push("/login");
- }
- }}
- />
- );
-};
-
-export default ChangePasswordDialog;
diff --git a/FrontEnd/src/app/views/settings/index.tsx b/FrontEnd/src/app/views/settings/index.tsx
deleted file mode 100644
index 04a2777a..00000000
--- a/FrontEnd/src/app/views/settings/index.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { useState } from "react";
-import { useHistory } from "react-router";
-import { useTranslation } from "react-i18next";
-import { Container, Form, Row, Col, Button, Modal } from "react-bootstrap";
-
-import { useUser, userService } from "@/services/user";
-
-import ChangePasswordDialog from "./ChangePasswordDialog";
-import ChangeAvatarDialog from "./ChangeAvatarDialog";
-import ChangeNicknameDialog from "./ChangeNicknameDialog";
-
-const ConfirmLogoutDialog: React.FC<{
- onClose: () => void;
- onConfirm: () => void;
-}> = ({ onClose, onConfirm }) => {
- const { t } = useTranslation();
-
- return (
- <Modal show centered onHide={onClose}>
- <Modal.Header>
- <Modal.Title className="text-danger">
- {t("settings.dialogConfirmLogout.title")}
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>{t("settings.dialogConfirmLogout.prompt")}</Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={onClose}>
- {t("operationDialog.cancel")}
- </Button>
- <Button variant="danger" onClick={onConfirm}>
- {t("operationDialog.confirm")}
- </Button>
- </Modal.Footer>
- </Modal>
- );
-};
-
-const SettingsPage: React.FC = (_) => {
- const { i18n, t } = useTranslation();
- const user = useUser();
- const history = useHistory();
-
- const [dialog, setDialog] = useState<
- null | "changepassword" | "changeavatar" | "changenickname" | "logout"
- >(null);
-
- const language = i18n.language.slice(0, 2);
-
- return (
- <>
- <Container>
- {user ? (
- <div className="cru-card my-3 py-3">
- <h3 className="px-3 mb-3 text-primary">
- {t("settings.subheaders.account")}
- </h3>
- <div
- className="settings-item clickable first"
- onClick={() => setDialog("changeavatar")}
- >
- {t("settings.changeAvatar")}
- </div>
- <div
- className="settings-item clickable"
- onClick={() => setDialog("changenickname")}
- >
- {t("settings.changeNickname")}
- </div>
- <div
- className="settings-item clickable text-danger"
- onClick={() => setDialog("changepassword")}
- >
- {t("settings.changePassword")}
- </div>
- <div
- className="settings-item clickable text-danger"
- onClick={() => {
- setDialog("logout");
- }}
- >
- {t("settings.logout")}
- </div>
- </div>
- ) : null}
- <div className="cru-card my-3 py-3">
- <h3 className="px-3 mb-3 text-primary">
- {t("settings.subheaders.customization")}
- </h3>
- <Row className="settings-item first mx-0">
- <Col xs="12" sm="auto">
- <div>{t("settings.languagePrimary")}</div>
- <small className="d-block text-secondary">
- {t("settings.languageSecondary")}
- </small>
- </Col>
- <Col xs="auto" className="ms-auto">
- <Form.Control
- as="select"
- value={language}
- onChange={(e) => {
- void i18n.changeLanguage(e.target.value);
- }}
- >
- <option value="zh">中文</option>
- <option value="en">English</option>
- </Form.Control>
- </Col>
- </Row>
- </div>
- </Container>
- {(() => {
- switch (dialog) {
- case "changepassword":
- return <ChangePasswordDialog open close={() => setDialog(null)} />;
- case "logout":
- return (
- <ConfirmLogoutDialog
- onClose={() => setDialog(null)}
- onConfirm={() => {
- void userService.logout().then(() => {
- history.push("/");
- });
- }}
- />
- );
- case "changeavatar":
- return <ChangeAvatarDialog open close={() => setDialog(null)} />;
- case "changenickname":
- return <ChangeNicknameDialog open close={() => setDialog(null)} />;
- default:
- return null;
- }
- })()}
- </>
- );
-};
-
-export default SettingsPage;
diff --git a/FrontEnd/src/app/views/settings/settings.sass b/FrontEnd/src/app/views/settings/settings.sass
deleted file mode 100644
index 8c6d24b8..00000000
--- a/FrontEnd/src/app/views/settings/settings.sass
+++ /dev/null
@@ -1,14 +0,0 @@
-.settings-item
- padding: 0.5em 1em
- transition: background 0.3s
- border-bottom: 1px solid $gray-200
-
- &.first
- border-top: 1px solid $gray-200
-
- &.clickable
- cursor: pointer
-
- &:hover
- background: $gray-300
-