import { useState, ChangeEvent, ComponentPropsWithoutRef } from "react"; import { useC, Text, UiLogicError } from "~src/common"; import { useUser } from "~src/services/user"; import { getHttpUserClient } from "~src/http/user"; import { ImageCropper, useImageCrop } from "~src/components/ImageCropper"; import BlobImage from "~src/components/BlobImage"; import { ButtonRowV2 } from "~src/components/button"; import { Dialog, DialogContainer } from "~src/components/dialog"; import "./ChangeAvatarDialog.css"; export default function ChangeAvatarDialog({ open, onClose, }: { open: boolean; onClose: () => void; }) { const c = useC(); const user = useUser(); type State = | "select" | "crop" | "process-crop" | "preview" | "uploading" | "success" | "error"; const [state, setState] = useState("select"); const [file, setFile] = useState(null); const { canCrop, crop, imageCropperProps } = useImageCrop(file, { constraint: { ratio: 1, }, }); const [resultBlob, setResultBlob] = useState(null); const [message, setMessage] = useState( "settings.dialogChangeAvatar.prompt.select", ); const close = () => { if (state !== "uploading") onClose(); }; const onSelectFile = (e: ChangeEvent): void => { const files = e.target.files; if (files == null || files.length === 0) { setFile(null); } else { setFile(files[0]); } }; const onCropNext = () => { if (!canCrop) { throw new UiLogicError(); } setState("process-crop"); void crop().then((b) => { setState("preview"); setResultBlob(b); }); }; const onCropPrevious = () => { setFile(null); setState("select"); }; const onPreviewPrevious = () => { setState("crop"); }; const upload = () => { if (resultBlob == null) { throw new UiLogicError(); } if (user == null) { throw new UiLogicError(); } setState("uploading"); getHttpUserClient() .putAvatar(user.username, resultBlob) .then( () => { setState("success"); }, () => { setState("error"); setMessage("operationDialog.error"); }, ); }; const cancelButton = { key: "cancel", text: "operationDialog.cancel", onClick: close, } as const; const createPreviousButton = (onClick: () => void) => ({ key: "previous", text: "operationDialog.previousStep", onClick, }) as const; const buttonsMap: Record< State, ComponentPropsWithoutRef["buttons"] > = { select: [ cancelButton, { key: "next", action: "major", text: "operationDialog.nextStep", onClick: () => setState("crop"), disabled: file == null, }, ], crop: [ cancelButton, createPreviousButton(onCropPrevious), { key: "next", action: "major", text: "operationDialog.nextStep", onClick: onCropNext, disabled: !canCrop, }, ], "process-crop": [cancelButton, createPreviousButton(onPreviewPrevious)], preview: [ cancelButton, createPreviousButton(onPreviewPrevious), { key: "upload", action: "major", text: "settings.dialogChangeAvatar.upload", onClick: upload, }, ], uploading: [], success: [ { key: "ok", text: "operationDialog.ok", color: "create", onClick: close, }, ], error: [ cancelButton, { key: "retry", action: "major", text: "operationDialog.retry", onClick: upload, }, ], }; return ( {(() => { if (state === "select") { return (
{c("settings.dialogChangeAvatar.prompt.select")}
); } else if (state === "crop") { if (file == null) { throw new UiLogicError(); } return (
{c("settings.dialogChangeAvatar.prompt.crop")}
); } else if (state === "process-crop") { return (
{c("settings.dialogChangeAvatar.prompt.processingCrop")}
); } else if (state === "preview") { return (
{c("settings.dialogChangeAvatar.prompt.preview")}
); } else if (state === "uploading") { return (
{c("settings.dialogChangeAvatar.prompt.uploading")}
); } else if (state === "success") { return (
{c("operationDialog.success")}
); } else { return (
{c(message)}
); } })()}
); }