import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Modal, ModalHeader, Row, Button, ModalBody, ModalFooter, } from 'reactstrap'; import { AxiosError } from 'axios'; import ImageCropper, { Clip, applyClipToImage } from '../common/ImageCropper'; import { UiLogicError } from '../common'; export interface ChangeAvatarDialogProps { open: boolean; close: () => void; process: (blob: Blob) => Promise; } const ChangeAvatarDialog: React.FC = (props) => { const { t } = useTranslation(); const [file, setFile] = React.useState(null); const [fileUrl, setFileUrl] = React.useState(null); const [clip, setClip] = React.useState(null); const [ cropImgElement, setCropImgElement, ] = React.useState(null); const [resultBlob, setResultBlob] = React.useState(null); const [resultUrl, setResultUrl] = React.useState(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 toggle = 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): 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 ( {t('userPage.dialogChangeAvatar.previewImgAlt')} ); }; return ( {t('userPage.dialogChangeAvatar.title')} {(() => { if (state === 'select') { return ( <> {t('userPage.dialogChangeAvatar.prompt.select')} ); } else if (state === 'crop') { if (fileUrl == null) { throw new UiLogicError(); } return ( <> {t('userPage.dialogChangeAvatar.prompt.crop')} ); } else if (state === 'processcrop') { return ( <> {t('userPage.dialogChangeAvatar.prompt.processingCrop')} ); } else if (state === 'preview') { return ( <> {createPreviewRow()} {t('userPage.dialogChangeAvatar.prompt.preview')} ); } else if (state === 'uploading') { return ( <> {createPreviewRow()} {t('userPage.dialogChangeAvatar.prompt.uploading')} ); } else if (state === 'success') { return ( <> {t('operationDialog.success')} ); } else { return ( <> {createPreviewRow()} {trueMessage} ); } })()} ); }; export default ChangeAvatarDialog;