diff options
| author | crupest <crupest@outlook.com> | 2020-10-31 00:42:06 +0800 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-10-31 00:42:06 +0800 | 
| commit | a3c97f6fb6313da2e8c0fac0b4c08f2ef4265d0f (patch) | |
| tree | ee006874b0c93e9bfc76f141a092a8b9585a1f95 /FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx | |
| parent | 0c4caaebe2480e77918d5d7df234f0edaeab74ba (diff) | |
| parent | 7ce0846d9ec968da3ea4f7ebcc6db26db8e49089 (diff) | |
| download | timeline-a3c97f6fb6313da2e8c0fac0b4c08f2ef4265d0f.tar.gz timeline-a3c97f6fb6313da2e8c0fac0b4c08f2ef4265d0f.tar.bz2 timeline-a3c97f6fb6313da2e8c0fac0b4c08f2ef4265d0f.zip | |
Merge pull request #161 from crupest/upgrade
Upgrade packages and split front end and back end.
Diffstat (limited to 'FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx')
| -rw-r--r-- | FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx | 302 | 
1 files changed, 302 insertions, 0 deletions
| diff --git a/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx b/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx new file mode 100644 index 00000000..ffa2218b --- /dev/null +++ b/FrontEnd/src/app/views/user/ChangeAvatarDialog.tsx @@ -0,0 +1,302 @@ +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 ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; + +export interface ChangeAvatarDialogProps { +  open: boolean; +  close: () => void; +  process: (blob: Blob) => Promise<void>; +} + +const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { +  const { t } = useTranslation(); + +  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 +  >("userPage.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 process = props.process; + +  const upload = React.useCallback(() => { +    if (resultBlob == null) { +      throw new UiLogicError(); +    } + +    setState("uploading"); +    process(resultBlob).then( +      () => { +        setState("success"); +      }, +      (e: unknown) => { +        setState("error"); +        setMessage({ type: "custom", text: (e as AxiosError).message }); +      } +    ); +  }, [resultBlob, process]); + +  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("userPage.dialogChangeAvatar.previewImgAlt")} +        /> +      </Row> +    ); +  }; + +  return ( +    <Modal show={props.open} onHide={close}> +      <Modal.Header> +        <Modal.Title> {t("userPage.dialogChangeAvatar.title")}</Modal.Title> +      </Modal.Header> +      {(() => { +        if (state === "select") { +          return ( +            <> +              <Modal.Body className="container"> +                <Row>{t("userPage.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("userPage.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("userPage.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("userPage.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("userPage.dialogChangeAvatar.upload")} +                </Button> +              </Modal.Footer> +            </> +          ); +        } else if (state === "uploading") { +          return ( +            <> +              <Modal.Body className="container"> +                {createPreviewRow()} +                <Row>{t("userPage.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; | 
