aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx')
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx334
1 files changed, 123 insertions, 211 deletions
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>
);
}
})()}