aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/pages
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-08-19 02:13:26 +0800
committercrupest <crupest@outlook.com>2023-08-19 02:13:26 +0800
commitd6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73 (patch)
tree4b546fe67049a8211b3265a5d3316ae3947ac6e7 /FrontEnd/src/pages
parenteec2e74a928f6448a0503e003d8afa693730b365 (diff)
downloadtimeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.tar.gz
timeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.tar.bz2
timeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.zip
...
Diffstat (limited to 'FrontEnd/src/pages')
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.css21
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx334
-rw-r--r--FrontEnd/src/pages/setting/index.css10
-rw-r--r--FrontEnd/src/pages/setting/index.tsx63
4 files changed, 177 insertions, 251 deletions
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.css b/FrontEnd/src/pages/setting/ChangeAvatarDialog.css
new file mode 100644
index 00000000..2aa0bb54
--- /dev/null
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.css
@@ -0,0 +1,21 @@
+.change-avatar-dialog-prompt {
+ margin: 0.5em 0;
+}
+
+.change-avatar-dialog-prompt.success {
+ color: var(--cru-create-color);
+}
+
+.change-avatar-dialog-prompt.error {
+ color: var(--cru-danger-color);
+}
+
+.change-avatar-cropper {
+ max-height: 400px;
+}
+
+.change-avatar-preview-image {
+ min-width: 50%;
+ max-width: 100%;
+ max-height: 300px;
+} \ No newline at end of file
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
index 7ac7dcad..f35fc5a7 100644
--- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
@@ -1,10 +1,4 @@
-import {
- useState,
- useEffect,
- ChangeEvent,
- ComponentPropsWithoutRef,
-} from "react";
-import { AxiosError } from "axios";
+import { useState, ChangeEvent, ComponentPropsWithoutRef } from "react";
import { useC, Text, UiLogicError } from "@/common";
@@ -16,11 +10,13 @@ import ImageCropper, {
Clip,
applyClipToImage,
} from "@/views/common/ImageCropper";
-import Button from "@/views/common/button/Button";
-import ButtonRow from "@/views/common/button/ButtonRow";
+import BlobImage from "@/views/common/BlobImage";
+import ButtonRowV2 from "@/views/common/button/ButtonRowV2";
import Dialog from "@/views/common/dialog/Dialog";
import DialogContainer from "@/views/common/dialog/DialogContainer";
+import "./ChangeAvatarDialog.css";
+
interface ChangeAvatarDialogProps {
open: boolean;
onClose: () => void;
@@ -34,15 +30,6 @@ export default function ChangeAvatarDialog({
const user = useUser();
- const [file, setFile] = useState<File | null>(null);
- const [fileUrl, setFileUrl] = useState<string | null>(null);
- const [clip, setClip] = useState<Clip | null>(null);
- const [cropImgElement, setCropImgElement] = useState<HTMLImageElement | null>(
- null,
- );
- const [resultBlob, setResultBlob] = useState<Blob | null>(null);
- const [resultUrl, setResultUrl] = useState<string | null>(null);
-
type State =
| "select"
| "crop"
@@ -53,46 +40,22 @@ export default function ChangeAvatarDialog({
| "error";
const [state, setState] = useState<State>("select");
+ const [file, setFile] = useState<File | null>(null);
+ const [clip, setClip] = useState<Clip | null>(null);
+ const [cropImgElement, setCropImgElement] = useState<HTMLImageElement | null>(
+ null,
+ );
+ const [resultBlob, setResultBlob] = useState<Blob | null>(null);
const [message, setMessage] = useState<Text>(
"settings.dialogChangeAvatar.prompt.select",
);
- const trueMessage = c(message);
-
const close = (): void => {
- if (!(state === "uploading")) {
+ if (state !== "uploading") {
onClose();
}
};
- 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]);
-
- useEffect(() => {
- if (resultBlob != null) {
- const url = URL.createObjectURL(resultBlob);
- setResultUrl(url);
- setState("preview");
- return () => {
- URL.revokeObjectURL(url);
- };
- } else {
- setResultUrl(null);
- }
- }, [resultBlob]);
-
const onSelectFile = (e: ChangeEvent<HTMLInputElement>): void => {
const files = e.target.files;
if (files == null || files.length === 0) {
@@ -113,7 +76,9 @@ export default function ChangeAvatarDialog({
}
setState("process-crop");
+
void applyClipToImage(cropImgElement, clip, file.type).then((b) => {
+ setState("preview");
setResultBlob(b);
});
};
@@ -124,7 +89,6 @@ export default function ChangeAvatarDialog({
};
const onPreviewPrevious = () => {
- setResultBlob(null);
setState("crop");
};
@@ -144,77 +108,80 @@ export default function ChangeAvatarDialog({
() => {
setState("success");
},
- (e: unknown) => {
+ () => {
setState("error");
- setMessage({ type: "custom", value: (e as AxiosError).message });
+ setMessage("operationDialog.error");
},
);
};
- 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={c("settings.dialogChangeAvatar.previewImgAlt") ?? undefined}
- alt={c("settings.dialogChangeAvatar.previewImgAlt") ?? undefined}
- />
- </div>
- </div>
- );
- };
+ const cancelButton = {
+ key: "cancel",
+ action: "secondary",
+ text: "operationDialog.cancel",
+ onClick: close,
+ } as const;
+
+ const createPreviousButton = (onClick: () => void) =>
+ ({
+ key: "previous",
+ action: "secondary",
+ text: "operationDialog.previousStep",
+ onClick,
+ }) as const;
const buttonsMap: Record<
State,
- ComponentPropsWithoutRef<typeof ButtonRow>["buttons"]
+ ComponentPropsWithoutRef<typeof ButtonRowV2>["buttons"]
> = {
select: [
+ cancelButton,
{
- key: "cancel",
- type: "normal",
- props: {
- outline: true,
- color: "secondary",
- text: "operationDialog.cancel",
- onClick: close,
- },
+ key: "next",
+ action: "primary",
+ text: "operationDialog.nextStep",
+ onClick: () => setState("crop"),
+ disabled: file == null,
},
],
crop: [
+ cancelButton,
+ createPreviousButton(onCropPrevious),
{
- key: "cancel",
- type: "normal",
- props: {
- outline: true,
- color: "secondary",
- text: "operationDialog.cancel",
- onClick: close,
- },
+ key: "next",
+ action: "primary",
+ text: "operationDialog.nextStep",
+ onClick: onCropNext,
+ disabled: cropImgElement == null || clip == null || clip.width === 0,
},
+ ],
+ "process-crop": [cancelButton, createPreviousButton(onPreviewPrevious)],
+ preview: [
+ cancelButton,
+ createPreviousButton(onPreviewPrevious),
{
- key: "previous",
- type: "normal",
- props: {
- outline: true,
- color: "secondary",
- text: "operationDialog.previousStep",
- onClick: onCropPrevious,
- },
+ key: "upload",
+ action: "primary",
+ text: "settings.dialogChangeAvatar.upload",
+ onClick: upload,
},
+ ],
+ uploading: [],
+ success: [
{
- key: "next",
- type: "normal",
- props: {
- color: "primary",
- text: "operationDialog.nextStep",
- onClick: onCropNext,
- disabled: cropImgElement == null || clip == null || clip.width === 0,
- },
+ key: "ok",
+ text: "operationDialog.ok",
+ color: "create",
+ onClick: close,
+ },
+ ],
+ error: [
+ cancelButton,
+ {
+ key: "retry",
+ action: "primary",
+ text: "operationDialog.retry",
+ onClick: upload,
},
],
};
@@ -224,150 +191,95 @@ export default function ChangeAvatarDialog({
<DialogContainer
title="settings.dialogChangeAvatar.title"
titleColor="primary"
- buttons={buttonsMap[state]}
+ buttonsV2={buttonsMap[state]}
>
{(() => {
if (state === "select") {
return (
- <div className="">
- <div className="row">
+ <div className="change-avatar-dialog-container">
+ <div className="change-avatar-dialog-prompt">
{c("settings.dialogChangeAvatar.prompt.select")}
</div>
- <div className="row">
- <input
- className="px-0"
- type="file"
- accept="image/*"
- onChange={onSelectFile}
- />
- </div>
+ <input
+ className="change-avatar-select-input"
+ type="file"
+ accept="image/*"
+ onChange={onSelectFile}
+ />
</div>
);
} else if (state === "crop") {
- if (fileUrl == null) {
+ if (file == 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">
+ <div className="change-avatar-dialog-container">
+ <ImageCropper
+ className="change-avatar-cropper"
+ clip={clip}
+ onChange={setClip}
+ image={file}
+ imageElementCallback={setCropImgElement}
+ />
+ <div className="change-avatar-dialog-prompt">
{c("settings.dialogChangeAvatar.prompt.crop")}
</div>
</div>
);
} else if (state === "process-crop") {
return (
- <>
- <div className="container">
- <div className="row">
- {c("settings.dialogChangeAvatar.prompt.processingCrop")}
- </div>
+ <div className="change-avatar-dialog-container">
+ <div className="change-avatar-dialog-prompt">
+ {c("settings.dialogChangeAvatar.prompt.processingCrop")}
</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>
- </>
+ </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 className="change-avatar-dialog-container">
+ <BlobImage
+ className="change-avatar-preview-image"
+ src={resultBlob}
+ alt={
+ c("settings.dialogChangeAvatar.previewImgAlt") ?? undefined
+ }
+ />
+ <div className="change-avatar-dialog-prompt">
+ {c("settings.dialogChangeAvatar.prompt.preview")}
</div>
- </>
+ </div>
);
} else if (state === "uploading") {
return (
- <>
- <div className="container">
- {createPreviewRow()}
- <div className="row">
- {t("settings.dialogChangeAvatar.prompt.uploading")}
- </div>
+ <div className="change-avatar-dialog-container">
+ <BlobImage
+ className="change-avatar-preview-image"
+ src={resultBlob}
+ />
+ <div className="change-avatar-dialog-prompt">
+ {c("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 className="change-avatar-dialog-container">
+ <div className="change-avatar-dialog-prompt success">
+ {c("operationDialog.success")}
</div>
- </>
+ </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 className="change-avatar-dialog-container">
+ <BlobImage
+ className="change-avatar-preview-image"
+ src={resultBlob}
+ />
+ <div className="change-avatar-dialog-prompt error">
+ {c(message)}
</div>
- </>
+ </div>
);
}
})()}
diff --git a/FrontEnd/src/pages/setting/index.css b/FrontEnd/src/pages/setting/index.css
index 8af65e93..86ccf706 100644
--- a/FrontEnd/src/pages/setting/index.css
+++ b/FrontEnd/src/pages/setting/index.css
@@ -1,15 +1,5 @@
/* TODO: Make item prettier. */
-.change-avatar-cropper-row {
- max-height: 400px;
-}
-
-.change-avatar-img {
- min-width: 50%;
- max-width: 100%;
- max-height: 400px;
-}
-
.setting-section {
padding: 1em 0;
margin: 1em 0;
diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx
index 8673d55a..4e0bf27e 100644
--- a/FrontEnd/src/pages/setting/index.tsx
+++ b/FrontEnd/src/pages/setting/index.tsx
@@ -156,33 +156,34 @@ function RegisterCodeSettingItem() {
}, [user, registerCode]);
return (
- <SettingItemContainer
- title="settings.myRegisterCode"
- description="settings.myRegisterCodeDesc"
- className="register-code-setting-item"
- onClick={() => setDialogOpen(true)}
- >
- {registerCode === undefined ? (
- <Spinner />
- ) : registerCode === null ? (
- <span>Noop</span>
- ) : (
- <code
- className="register-code"
- onClick={(event) => {
- void navigator.clipboard.writeText(registerCode).then(() => {
- pushAlert({
- type: "create",
- message: "settings.myRegisterCodeCopied",
+ <>
+ <SettingItemContainer
+ title="settings.myRegisterCode"
+ description="settings.myRegisterCodeDesc"
+ className="register-code-setting-item"
+ onClick={() => setDialogOpen(true)}
+ >
+ {registerCode === undefined ? (
+ <Spinner />
+ ) : registerCode === null ? (
+ <span>Noop</span>
+ ) : (
+ <code
+ className="register-code"
+ onClick={(event) => {
+ void navigator.clipboard.writeText(registerCode).then(() => {
+ pushAlert({
+ type: "create",
+ message: "settings.myRegisterCodeCopied",
+ });
});
- });
- event.stopPropagation();
- }}
- >
- {registerCode}
- </code>
- )}
-
+ event.stopPropagation();
+ }}
+ >
+ {registerCode}
+ </code>
+ )}
+ </SettingItemContainer>
<ConfirmDialog
title="settings.renewRegisterCode"
body="settings.renewRegisterCodeDesc"
@@ -196,8 +197,8 @@ function RegisterCodeSettingItem() {
setRegisterCode(undefined);
});
}}
- />
- </SettingItemContainer>
+ />{" "}
+ </>
);
}
@@ -243,7 +244,6 @@ export default function SettingPage() {
"change-avatar",
"change-nickname",
"logout",
- "renew-register-code",
]);
return (
@@ -271,7 +271,10 @@ export default function SettingPage() {
/>
</SettingSection>
) : null}
- <SettingSection title="settings.subheader.customization" color="secondary">
+ <SettingSection
+ title="settings.subheader.customization"
+ color="secondary"
+ >
<LanguageChangeSettingItem />
</SettingSection>
<ChangePasswordDialog {...dialogPropsMap["change-password"]} />