import invariant from "invariant"; import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import AvatarEditor from "react-avatar-editor"; import Dropzone from "react-dropzone"; import styled from "styled-components"; import { s } from "@shared/styles"; import { AttachmentPreset } from "@shared/types"; import { AttachmentValidation } from "@shared/validations"; import RootStore from "~/stores/RootStore"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; import LoadingIndicator from "~/components/LoadingIndicator"; import Modal from "~/components/Modal"; import withStores from "~/components/withStores"; import { compressImage } from "~/utils/compressImage"; import { uploadFile, dataUrlToBlob } from "~/utils/files"; export type Props = { onSuccess: (url: string) => void | Promise; onError: (error: string) => void; submitText?: string; borderRadius?: number; }; @observer class ImageUpload extends React.Component { @observable isUploading = false; @observable isCropping = false; @observable zoom = 1; @observable file: File; avatarEditorRef = React.createRef(); static defaultProps = { submitText: "Crop Image", borderRadius: 150, }; onDropAccepted = async (files: File[]) => { this.isCropping = true; this.file = files[0]; }; handleCrop = () => { this.isUploading = true; // allow the UI to update before converting the canvas to a Blob // for large images this can cause the page rendering to hang. setTimeout(this.uploadImage, 0); }; uploadImage = async () => { const canvas = this.avatarEditorRef.current?.getImage(); invariant(canvas, "canvas is not defined"); const imageBlob = dataUrlToBlob(canvas.toDataURL()); try { const compressed = await compressImage(imageBlob, { maxHeight: 512, maxWidth: 512, }); const attachment = await uploadFile(compressed, { name: this.file.name, preset: AttachmentPreset.Avatar, }); this.props.onSuccess(attachment.url); } catch (err) { this.props.onError(err.message); } finally { this.isUploading = false; this.isCropping = false; } }; handleClose = () => { this.isUploading = false; this.isCropping = false; }; handleZoom = (event: React.ChangeEvent) => { const target = event.target; if (target instanceof HTMLInputElement) { this.zoom = parseFloat(target.value); } }; renderCropping() { const { ui, submitText } = this.props; return ( {this.isUploading && } {this.isUploading ? "Uploading…" : submitText} ); } render() { if (this.isCropping) { return this.renderCropping(); } return ( {({ getRootProps, getInputProps }) => (
{this.props.children}
)}
); } } const AvatarEditorContainer = styled(Flex)` margin-bottom: 30px; `; const RangeInput = styled.input` display: block; width: 300px; margin-bottom: 30px; height: 4px; cursor: var(--pointer); color: inherit; border-radius: 99999px; background-color: #dee1e3; appearance: none; &::-webkit-slider-thumb { -webkit-appearance: none; height: 16px; width: 16px; border-radius: 50%; background: ${s("text")}; cursor: var(--pointer); } &:focus { outline: none; } `; const CropButton = styled(Button)` width: 300px; `; export default withStores(ImageUpload);