Web: Combining avatar Editor and IconEditor components into one component and removing avatarEditor and IconEditor components
This commit is contained in:
parent
11e7a777f6
commit
49bfcf917a
@ -1,210 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { ReactSVG } from "react-svg";
|
||||
import throttle from "lodash/throttle";
|
||||
import AvatarEditor from "react-avatar-editor";
|
||||
|
||||
import Slider from "@docspace/components/slider";
|
||||
import IconButton from "@docspace/components/icon-button";
|
||||
import { Base } from "@docspace/components/themes";
|
||||
|
||||
const StyledAvatarCropper = styled.div`
|
||||
max-width: 216px;
|
||||
|
||||
.icon_cropper-crop_area {
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
.icon_cropper-grid {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
svg {
|
||||
opacity: 0.2;
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.gridColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_cropper-delete_button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.background};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.borderColor};
|
||||
border-radius: 3px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBackground};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBorderColor};
|
||||
}
|
||||
|
||||
&-text {
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.color};
|
||||
}
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.iconColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_cropper-zoom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&-slider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&-button {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledAvatarCropper.defaultProps = { theme: Base };
|
||||
|
||||
const AvatarCropper = ({
|
||||
t,
|
||||
avatar,
|
||||
onChangeAvatar,
|
||||
uploadedFile,
|
||||
setUploadedFile,
|
||||
setPreviewAvatar,
|
||||
}) => {
|
||||
let editorRef = null;
|
||||
const setEditorRef = (editor) => (editorRef = editor);
|
||||
|
||||
const handlePositionChange = (position) =>
|
||||
onChangeAvatar({ ...avatar, x: position.x, y: position.y });
|
||||
|
||||
const handleSliderChange = (e, newZoom = null) =>
|
||||
onChangeAvatar({ ...avatar, zoom: newZoom ? newZoom : +e.target.value });
|
||||
|
||||
const handleZoomInClick = () =>
|
||||
handleSliderChange({}, avatar.zoom <= 4.5 ? avatar.zoom + 0.5 : 5);
|
||||
|
||||
const handleZoomOutClick = () =>
|
||||
handleSliderChange({}, avatar.zoom >= 1.5 ? avatar.zoom - 0.5 : 1);
|
||||
|
||||
const handleDeleteImage = () => setUploadedFile(null);
|
||||
|
||||
const handleImageChange = throttle(() => {
|
||||
try {
|
||||
if (!editorRef) return;
|
||||
const newPreveiwImage = editorRef.getImageScaledToCanvas()?.toDataURL();
|
||||
setPreviewAvatar(newPreveiwImage);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
handleImageChange();
|
||||
return () => {
|
||||
setPreviewAvatar("");
|
||||
};
|
||||
}, [avatar]);
|
||||
|
||||
return (
|
||||
<StyledAvatarCropper className="icon_cropper">
|
||||
<div className="icon_cropper-crop_area">
|
||||
<ReactSVG
|
||||
className="icon_cropper-grid"
|
||||
src="images/icon-cropper-grid.svg"
|
||||
/>
|
||||
<AvatarEditor
|
||||
ref={setEditorRef}
|
||||
image={uploadedFile}
|
||||
width={216}
|
||||
height={216}
|
||||
position={{ x: avatar.x, y: avatar.y }}
|
||||
scale={avatar.zoom}
|
||||
color={[6, 22, 38, 0.2]}
|
||||
border={0}
|
||||
rotate={0}
|
||||
borderRadius={108}
|
||||
onPositionChange={handlePositionChange}
|
||||
onImageReady={handleImageChange}
|
||||
disableHiDPIScaling
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="icon_cropper-delete_button"
|
||||
onClick={handleDeleteImage}
|
||||
title={t("Common:Delete")}
|
||||
>
|
||||
<ReactSVG src={"images/trash.react.svg"} />
|
||||
<div className="icon_cropper-delete_button-text">
|
||||
{t("Common:Delete")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="icon_cropper-zoom-container">
|
||||
<IconButton
|
||||
className="icon_cropper-zoom-container-button"
|
||||
size="16"
|
||||
onClick={handleZoomOutClick}
|
||||
iconName={"/static/images/zoom-minus.react.svg"}
|
||||
isFill={true}
|
||||
isClickable={false}
|
||||
/>
|
||||
<Slider
|
||||
className="icon_cropper-zoom-container-slider"
|
||||
max={5}
|
||||
min={1}
|
||||
onChange={handleSliderChange}
|
||||
step={0.01}
|
||||
value={avatar.zoom}
|
||||
/>
|
||||
<IconButton
|
||||
className="icon_cropper-zoom-container-button"
|
||||
size="16"
|
||||
onClick={handleZoomInClick}
|
||||
iconName={"/static/images/zoom-plus.react.svg"}
|
||||
isFill={true}
|
||||
isClickable={false}
|
||||
/>
|
||||
</div>
|
||||
</StyledAvatarCropper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarCropper;
|
@ -1,56 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import AvatarCropper from "./avatar-cropper";
|
||||
import AvatarPreview from "./preview";
|
||||
import Dropzone from "./dropzone";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
.avatar-editor {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const AvatarEditor = ({
|
||||
t,
|
||||
profile,
|
||||
avatar,
|
||||
onChangeAvatar,
|
||||
preview,
|
||||
setPreview,
|
||||
}) => {
|
||||
const setUploadedFile = (file) =>
|
||||
onChangeAvatar({ ...avatar, uploadedFile: file });
|
||||
|
||||
const isDefaultAvatar =
|
||||
typeof avatar.uploadedFile === "string" &&
|
||||
avatar.uploadedFile.includes("default_user_photo");
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{avatar.uploadedFile && !isDefaultAvatar && (
|
||||
<div className="avatar-editor">
|
||||
<AvatarCropper
|
||||
t={t}
|
||||
avatar={avatar}
|
||||
onChangeAvatar={onChangeAvatar}
|
||||
uploadedFile={avatar.uploadedFile}
|
||||
setUploadedFile={setUploadedFile}
|
||||
setPreviewAvatar={setPreview}
|
||||
/>
|
||||
<AvatarPreview avatar={preview} userName={profile.displayName} />
|
||||
</div>
|
||||
)}
|
||||
<Dropzone t={t} setUploadedFile={setUploadedFile} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarEditor;
|
@ -13,12 +13,32 @@ import {
|
||||
deleteAvatar,
|
||||
} from "@docspace/common/api/people";
|
||||
import { dataUrlToFile } from "@docspace/common/utils/dataUrlToFile";
|
||||
import AvatarEditor from "./editor";
|
||||
import ImageEditor from "@docspace/components/ImageEditor";
|
||||
import AvatarPreview from "@docspace/components/ImageEditor/AvatarPreview";
|
||||
|
||||
const StyledModalDialog = styled(ModalDialog)``;
|
||||
const StyledModalDialog = styled(ModalDialog)`
|
||||
.wrapper-image-editor {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
.avatar-editor {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AvatarEditorDialog = (props) => {
|
||||
const { t } = useTranslation(["Profile", "PeopleTranslations", "ProfileAction", "Common"]);
|
||||
const { t } = useTranslation([
|
||||
"Profile",
|
||||
"PeopleTranslations",
|
||||
"ProfileAction",
|
||||
"Common",
|
||||
"CreateEditRoomDialog",
|
||||
]);
|
||||
|
||||
const {
|
||||
visible,
|
||||
onClose,
|
||||
@ -89,13 +109,16 @@ const AvatarEditorDialog = (props) => {
|
||||
</Text>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<AvatarEditor
|
||||
<ImageEditor
|
||||
t={t}
|
||||
avatar={avatar}
|
||||
onChangeAvatar={onChangeAvatar}
|
||||
profile={profile}
|
||||
preview={preview}
|
||||
className="wrapper-image-editor"
|
||||
classNameWrapperImageCropper="avatar-editor"
|
||||
image={avatar}
|
||||
setPreview={setPreview}
|
||||
onChangeImage={onChangeAvatar}
|
||||
Preview={
|
||||
<AvatarPreview avatar={preview} userName={profile.displayName} />
|
||||
}
|
||||
/>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
|
@ -1,118 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import resizeImage from "resize-image";
|
||||
import { Base } from "@docspace/components/themes";
|
||||
import { ColorTheme, ThemeType } from "@docspace/common/components/ColorTheme";
|
||||
|
||||
const StyledDropzone = styled.div`
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
border: 2px dashed
|
||||
${(props) => props.theme.createEditRoomDialog.dropzone.borderColor};
|
||||
border-radius: 6px;
|
||||
|
||||
.dropzone {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
user-select: none;
|
||||
|
||||
&-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
&-main {
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 1px;
|
||||
}
|
||||
&-secondary {
|
||||
font-weight: 400;
|
||||
color: ${(props) =>
|
||||
props.theme.createEditRoomDialog.dropzone.linkSecondaryColor};
|
||||
}
|
||||
}
|
||||
|
||||
&-exsts {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: ${(props) => props.theme.createEditRoomDialog.dropzone.exstsColor};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledDropzone.defaultProps = { theme: Base };
|
||||
|
||||
const Dropzone = ({ t, setUploadedFile, isDisabled }) => {
|
||||
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
|
||||
maxFiles: 1,
|
||||
noClick: isDisabled,
|
||||
noKeyboard: isDisabled,
|
||||
// maxSize: 1000000,
|
||||
accept: ["image/png", "image/jpeg"],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (acceptedFiles.length) {
|
||||
const fr = new FileReader();
|
||||
fr.readAsDataURL(acceptedFiles[0]);
|
||||
|
||||
fr.onload = () => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = resizeImage.resize2Canvas(img, img.width, img.height);
|
||||
|
||||
const data = resizeImage.resize(
|
||||
canvas,
|
||||
img.width / 4,
|
||||
img.height / 4,
|
||||
resizeImage.JPEG
|
||||
);
|
||||
|
||||
fetch(data)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const file = new File([blob], "File name", {
|
||||
type: "image/jpg",
|
||||
});
|
||||
setUploadedFile(file);
|
||||
});
|
||||
};
|
||||
img.src = fr.result;
|
||||
};
|
||||
}
|
||||
}, [acceptedFiles]);
|
||||
|
||||
return (
|
||||
<StyledDropzone>
|
||||
<div {...getRootProps({ className: "dropzone" })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className="dropzone-link">
|
||||
<ColorTheme className="dropzone-link-main" themeId={ThemeType.Link}>
|
||||
{t("DropzoneTitleLink")}
|
||||
</ColorTheme>
|
||||
<span className="dropzone-link-secondary">
|
||||
{t("DropzoneTitleSecondary")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="dropzone-exsts">{t("DropzoneTitleExsts")}</div>
|
||||
</div>
|
||||
</StyledDropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropzone;
|
@ -1,227 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { ReactSVG } from "react-svg";
|
||||
import throttle from "lodash/throttle";
|
||||
import AvatarEditor from "react-avatar-editor";
|
||||
|
||||
import Slider from "@docspace/components/slider";
|
||||
import IconButton from "@docspace/components/icon-button";
|
||||
import { Base } from "@docspace/components/themes";
|
||||
|
||||
const StyledIconCropper = styled.div`
|
||||
max-width: 216px;
|
||||
|
||||
.icon_cropper-crop_area {
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
.icon_cropper-grid {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
svg {
|
||||
opacity: 0.2;
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.gridColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_cropper-delete_button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.background};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.borderColor};
|
||||
border-radius: 3px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBackground};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBorderColor};
|
||||
}
|
||||
|
||||
&-text {
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.color};
|
||||
}
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.iconColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_cropper-zoom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&-slider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&-button {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledIconCropper.defaultProps = { theme: Base };
|
||||
|
||||
const IconCropper = ({
|
||||
t,
|
||||
icon,
|
||||
onChangeIcon,
|
||||
uploadedFile,
|
||||
setUploadedFile,
|
||||
setPreviewIcon,
|
||||
isDisabled,
|
||||
}) => {
|
||||
let editorRef = null;
|
||||
const setEditorRef = (editor) => (editorRef = editor);
|
||||
|
||||
const handlePositionChange = (position) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
onChangeIcon({ ...icon, x: position.x, y: position.y });
|
||||
};
|
||||
|
||||
const handleSliderChange = (e, newZoom = null) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
onChangeIcon({ ...icon, zoom: newZoom ? newZoom : +e.target.value });
|
||||
};
|
||||
|
||||
const handleZoomInClick = () => {
|
||||
if (isDisabled) return;
|
||||
|
||||
handleSliderChange({}, icon.zoom <= 4.5 ? icon.zoom + 0.5 : 5);
|
||||
};
|
||||
const handleZoomOutClick = () => {
|
||||
if (isDisabled) return;
|
||||
|
||||
handleSliderChange({}, icon.zoom >= 1.5 ? icon.zoom - 0.5 : 1);
|
||||
};
|
||||
const handleDeleteImage = () => {
|
||||
if (isDisabled) return;
|
||||
setUploadedFile(null);
|
||||
};
|
||||
|
||||
const handleImageChange = throttle(() => {
|
||||
try {
|
||||
if (!editorRef) return;
|
||||
const newPreveiwImage = editorRef.getImageScaledToCanvas()?.toDataURL();
|
||||
setPreviewIcon(newPreveiwImage);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
handleImageChange();
|
||||
return () => {
|
||||
setPreviewIcon("");
|
||||
};
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<StyledIconCropper className="icon_cropper">
|
||||
<div className="icon_cropper-crop_area">
|
||||
<ReactSVG
|
||||
className="icon_cropper-grid"
|
||||
src="images/icon-cropper-grid.svg"
|
||||
/>
|
||||
<AvatarEditor
|
||||
ref={setEditorRef}
|
||||
image={uploadedFile}
|
||||
width={216}
|
||||
height={216}
|
||||
position={{ x: icon.x, y: icon.y }}
|
||||
scale={icon.zoom}
|
||||
color={[6, 22, 38, 0.2]}
|
||||
border={0}
|
||||
rotate={0}
|
||||
borderRadius={108}
|
||||
onPositionChange={handlePositionChange}
|
||||
onImageReady={handleImageChange}
|
||||
disableHiDPIScaling
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="icon_cropper-delete_button"
|
||||
onClick={handleDeleteImage}
|
||||
title={t("Common:Delete")}
|
||||
>
|
||||
<ReactSVG src={"images/trash.react.svg"} />
|
||||
<div className="icon_cropper-delete_button-text">
|
||||
{t("Common:Delete")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="icon_cropper-zoom-container">
|
||||
<IconButton
|
||||
className="icon_cropper-zoom-container-button"
|
||||
size="16"
|
||||
onClick={handleZoomOutClick}
|
||||
iconName={"/static/images/zoom-minus.react.svg"}
|
||||
isFill={true}
|
||||
isClickable={false}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<Slider
|
||||
className="icon_cropper-zoom-container-slider"
|
||||
max={5}
|
||||
min={1}
|
||||
onChange={handleSliderChange}
|
||||
step={0.01}
|
||||
value={icon.zoom}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<IconButton
|
||||
className="icon_cropper-zoom-container-button"
|
||||
size="16"
|
||||
onClick={handleZoomInClick}
|
||||
iconName={"/static/images/zoom-plus.react.svg"}
|
||||
isFill={true}
|
||||
isClickable={false}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
</StyledIconCropper>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconCropper;
|
@ -1,67 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import Dropzone from "./Dropzone";
|
||||
|
||||
const StyledIconEditor = styled.div`
|
||||
.icon-editor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: start;
|
||||
gap: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
import IconCropper from "./IconCropper";
|
||||
import PreviewTile from "./PreviewTile";
|
||||
|
||||
const IconEditor = ({
|
||||
t,
|
||||
isEdit,
|
||||
title,
|
||||
tags,
|
||||
defaultTagLabel,
|
||||
icon,
|
||||
onChangeIcon,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const [previewIcon, setPreviewIcon] = useState(null);
|
||||
|
||||
const setUploadedFile = (uploadedFile) =>
|
||||
onChangeIcon({ ...icon, uploadedFile });
|
||||
|
||||
return (
|
||||
<StyledIconEditor>
|
||||
{icon.uploadedFile && (
|
||||
<div className="icon-editor">
|
||||
<IconCropper
|
||||
t={t}
|
||||
icon={icon}
|
||||
onChangeIcon={onChangeIcon}
|
||||
uploadedFile={icon.uploadedFile}
|
||||
setUploadedFile={setUploadedFile}
|
||||
setPreviewIcon={setPreviewIcon}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
||||
<PreviewTile
|
||||
t={t}
|
||||
title={title || t("Files:NewRoom")}
|
||||
previewIcon={previewIcon}
|
||||
tags={tags.map((tag) => tag.name)}
|
||||
defaultTagLabel={defaultTagLabel}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Dropzone
|
||||
t={t}
|
||||
setUploadedFile={setUploadedFile}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</StyledIconEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconEditor;
|
@ -6,7 +6,7 @@ import RoomTypeDropdown from "./RoomTypeDropdown";
|
||||
import ThirdPartyStorage from "./ThirdPartyStorage";
|
||||
import TagInput from "./TagInput";
|
||||
import RoomType from "./RoomType";
|
||||
import IconEditor from "./IconEditor";
|
||||
|
||||
import PermanentSettings from "./PermanentSettings";
|
||||
import InputParam from "./Params/InputParam";
|
||||
import IsPrivateParam from "./IsPrivateParam";
|
||||
@ -15,11 +15,22 @@ import withLoader from "@docspace/client/src/HOCs/withLoader";
|
||||
import Loaders from "@docspace/common/components/Loaders";
|
||||
import { getRoomTypeDefaultTagTranslation } from "../data";
|
||||
|
||||
import ImageEditor from "@docspace/components/ImageEditor";
|
||||
import PreviewTile from "@docspace/components/ImageEditor/PreviewTile";
|
||||
|
||||
const StyledSetRoomParams = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 22px;
|
||||
|
||||
.icon-editor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: start;
|
||||
gap: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SetRoomParams = ({
|
||||
@ -35,6 +46,8 @@ const SetRoomParams = ({
|
||||
isValidTitle,
|
||||
setIsValidTitle,
|
||||
}) => {
|
||||
const [previewIcon, setPreviewIcon] = React.useState(null);
|
||||
|
||||
const onChangeName = (e) => {
|
||||
setIsValidTitle(true);
|
||||
setRoomParams({ ...roomParams, title: e.target.value });
|
||||
@ -111,14 +124,26 @@ const SetRoomParams = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconEditor
|
||||
<ImageEditor
|
||||
t={t}
|
||||
title={roomParams.title}
|
||||
tags={roomParams.tags}
|
||||
defaultTagLabel={getRoomTypeDefaultTagTranslation(roomParams.type, t)}
|
||||
icon={roomParams.icon}
|
||||
onChangeIcon={onChangeIcon}
|
||||
isDisabled={isDisabled}
|
||||
image={roomParams.icon}
|
||||
setPreview={setPreviewIcon}
|
||||
onChangeImage={onChangeIcon}
|
||||
classNameWrapperImageCropper={"icon-editor"}
|
||||
Preview={
|
||||
<PreviewTile
|
||||
t={t}
|
||||
title={roomParams.title || t("Files:NewRoom")}
|
||||
previewIcon={previewIcon}
|
||||
tags={roomParams.tags.map((tag) => tag.name)}
|
||||
isDisabled={isDisabled}
|
||||
defaultTagLabel={getRoomTypeDefaultTagTranslation(
|
||||
roomParams.type,
|
||||
t
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledSetRoomParams>
|
||||
);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import Avatar from "@docspace/components/avatar";
|
||||
import Text from "@docspace/components/text";
|
||||
import Avatar from "../../avatar";
|
||||
import Text from "../../text";
|
||||
|
||||
import { hugeMobile } from "@docspace/components/utils/device";
|
||||
import { hugeMobile } from "../../utils/device";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
@ -1,7 +1,7 @@
|
||||
import styled from "styled-components";
|
||||
import { hugeMobile } from "@docspace/components/utils/device";
|
||||
import { hugeMobile } from "../../utils/device";
|
||||
|
||||
import { Base } from "@docspace/components/themes";
|
||||
import { Base } from "../../themes";
|
||||
|
||||
const StyledDropzone = styled.div`
|
||||
cursor: pointer;
|
||||
@ -16,10 +16,8 @@ const StyledDropzone = styled.div`
|
||||
|
||||
.dropzone_loader {
|
||||
position: absolute;
|
||||
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@ -43,8 +41,6 @@ const StyledDropzone = styled.div`
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
&-main {
|
||||
color: ${(props) =>
|
||||
props.theme.createEditRoomDialog.dropzone.linkMainColor};
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
@ -1,24 +1,22 @@
|
||||
import React, { useState, useRef, useMemo, useEffect } from "react";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
|
||||
import StyledDropzone from "./StyledDropzone";
|
||||
import Loader from "@docspace/components/loader";
|
||||
|
||||
import { toastr } from "@docspace/components";
|
||||
import resizeImage from "resize-image";
|
||||
|
||||
import Loader from "../../loader";
|
||||
|
||||
import { toastr } from "../../";
|
||||
|
||||
import { ColorTheme, ThemeType } from "@docspace/common/components/ColorTheme";
|
||||
import StyledDropzone from "./StyledDropzone";
|
||||
|
||||
const ONE_MEGABYTE = 1024 * 1024;
|
||||
const COMPRESSION_RATIO = 2;
|
||||
const NO_COMPRESSION_RATIO = 1;
|
||||
|
||||
const Dropzone = ({ setUploadedFile }) => {
|
||||
const Dropzone = ({ t, setUploadedFile, isDisabled }) => {
|
||||
const [loadingFile, setLoadingFile] = useState(false);
|
||||
const { t } = useTranslation("CreateEditRoomDialog");
|
||||
|
||||
const mount = useRef(false);
|
||||
|
||||
const timer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -99,7 +97,7 @@ const Dropzone = ({ setUploadedFile }) => {
|
||||
error instanceof Error &&
|
||||
error.message === "recursion depth exceeded"
|
||||
) {
|
||||
toastr.error(t("SizeImageLarge"));
|
||||
toastr.error(t("CreateEditRoomDialog:SizeImageLarge"));
|
||||
}
|
||||
console.error(error);
|
||||
})
|
||||
@ -113,6 +111,8 @@ const Dropzone = ({ setUploadedFile }) => {
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
maxFiles: 1,
|
||||
noClick: isDisabled,
|
||||
noKeyboard: isDisabled,
|
||||
// maxSize: 1000000,
|
||||
accept: ["image/png", "image/jpeg"],
|
||||
onDrop,
|
||||
@ -126,12 +126,16 @@ const Dropzone = ({ setUploadedFile }) => {
|
||||
<div {...getRootProps({ className: "dropzone" })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className="dropzone-link">
|
||||
<span className="dropzone-link-main">{t("DropzoneTitleLink")}</span>
|
||||
<ColorTheme className="dropzone-link-main" themeId={ThemeType.Link}>
|
||||
{t("CreateEditRoomDialog:DropzoneTitleLink")}
|
||||
</ColorTheme>
|
||||
<span className="dropzone-link-secondary">
|
||||
{t("DropzoneTitleSecondary")}
|
||||
{t("CreateEditRoomDialog:DropzoneTitleSecondary")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="dropzone-exsts">{t("DropzoneTitleExsts")}</div>
|
||||
<div className="dropzone-exsts">
|
||||
{t("CreateEditRoomDialog:DropzoneTitleExsts")}
|
||||
</div>
|
||||
</div>
|
||||
</StyledDropzone>
|
||||
);
|
@ -0,0 +1,94 @@
|
||||
import styled from "styled-components";
|
||||
import { Base } from "../../themes";
|
||||
|
||||
const StyledImageCropper = styled.div`
|
||||
max-width: 216px;
|
||||
|
||||
.icon_cropper-crop_area {
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
.icon_cropper-grid {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
svg {
|
||||
opacity: 0.2;
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.gridColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_cropper-delete_button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.background};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.borderColor};
|
||||
border-radius: 3px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBackground};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBorderColor};
|
||||
}
|
||||
|
||||
&-text {
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.color};
|
||||
}
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.iconColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_cropper-zoom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&-slider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&-button {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledImageCropper.defaultProps = { theme: Base };
|
||||
export default StyledImageCropper;
|
136
packages/components/ImageEditor/ImageCropper/index.js
Normal file
136
packages/components/ImageEditor/ImageCropper/index.js
Normal file
@ -0,0 +1,136 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { ReactSVG } from "react-svg";
|
||||
import throttle from "lodash/throttle";
|
||||
import AvatarEditor from "react-avatar-editor";
|
||||
|
||||
import Slider from "../../slider";
|
||||
import IconButton from "../../icon-button";
|
||||
import StyledImageCropper from "./StyledImageCropper";
|
||||
|
||||
const ImageCropper = ({
|
||||
t,
|
||||
image,
|
||||
onChangeImage,
|
||||
uploadedFile,
|
||||
setUploadedFile,
|
||||
setPreviewImage,
|
||||
isDisabled,
|
||||
}) => {
|
||||
let editorRef = null;
|
||||
const setEditorRef = (editor) => (editorRef = editor);
|
||||
|
||||
const handlePositionChange = (position) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
onChangeImage({ ...image, x: position.x, y: position.y });
|
||||
};
|
||||
|
||||
const handleSliderChange = (e, newZoom = null) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
onChangeImage({ ...image, zoom: newZoom ? newZoom : +e.target.value });
|
||||
};
|
||||
|
||||
const handleZoomInClick = () => {
|
||||
if (isDisabled) return;
|
||||
|
||||
handleSliderChange({}, image.zoom <= 4.5 ? image.zoom + 0.5 : 5);
|
||||
};
|
||||
|
||||
const handleZoomOutClick = () => {
|
||||
if (isDisabled) return;
|
||||
|
||||
handleSliderChange({}, image.zoom >= 1.5 ? image.zoom - 0.5 : 1);
|
||||
};
|
||||
|
||||
const handleDeleteImage = () => {
|
||||
if (isDisabled) return;
|
||||
setUploadedFile(null);
|
||||
};
|
||||
|
||||
const handleImageChange = throttle(() => {
|
||||
try {
|
||||
if (!editorRef) return;
|
||||
const newPreveiwImage = editorRef.getImageScaledToCanvas()?.toDataURL();
|
||||
setPreviewImage(newPreveiwImage);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
handleImageChange();
|
||||
return () => {
|
||||
setPreviewImage("");
|
||||
};
|
||||
}, [image]);
|
||||
|
||||
return (
|
||||
<StyledImageCropper className="icon_cropper">
|
||||
<div className="icon_cropper-crop_area">
|
||||
<ReactSVG
|
||||
className="icon_cropper-grid"
|
||||
src="images/icon-cropper-grid.svg"
|
||||
/>
|
||||
<AvatarEditor
|
||||
ref={setEditorRef}
|
||||
image={uploadedFile}
|
||||
width={216}
|
||||
height={216}
|
||||
position={{ x: image.x, y: image.y }}
|
||||
scale={image.zoom}
|
||||
color={[6, 22, 38, 0.2]}
|
||||
border={0}
|
||||
rotate={0}
|
||||
borderRadius={108}
|
||||
onPositionChange={handlePositionChange}
|
||||
onImageReady={handleImageChange}
|
||||
disableHiDPIScaling
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="icon_cropper-delete_button"
|
||||
onClick={handleDeleteImage}
|
||||
title={t("Common:Delete")}
|
||||
>
|
||||
<ReactSVG src={"images/trash.react.svg"} />
|
||||
<div className="icon_cropper-delete_button-text">
|
||||
{t("Common:Delete")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="icon_cropper-zoom-container">
|
||||
<IconButton
|
||||
className="icon_cropper-zoom-container-button"
|
||||
size="16"
|
||||
onClick={handleZoomOutClick}
|
||||
iconName={"/static/images/zoom-minus.react.svg"}
|
||||
isFill={true}
|
||||
isClickable={false}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<Slider
|
||||
className="icon_cropper-zoom-container-slider"
|
||||
max={5}
|
||||
min={1}
|
||||
onChange={handleSliderChange}
|
||||
step={0.01}
|
||||
value={image.zoom}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<IconButton
|
||||
className="icon_cropper-zoom-container-button"
|
||||
size="16"
|
||||
onClick={handleZoomInClick}
|
||||
iconName={"/static/images/zoom-plus.react.svg"}
|
||||
isFill={true}
|
||||
isClickable={false}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
</StyledImageCropper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageCropper;
|
@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { smallTablet } from "@docspace/components/utils/device";
|
||||
|
||||
import Tags from "@docspace/common/components/Tags";
|
||||
import Tag from "@docspace/components/tag";
|
||||
import { Base } from "@docspace/components/themes";
|
||||
|
||||
import { smallTablet } from "../../utils/device";
|
||||
import Tag from "../../tag";
|
||||
import { Base } from "../../themes";
|
||||
|
||||
const StyledPreviewTile = styled.div`
|
||||
background: ${(props) =>
|
||||
@ -73,7 +73,7 @@ const StyledPreviewTile = styled.div`
|
||||
`;
|
||||
StyledPreviewTile.defaultProps = { theme: Base };
|
||||
|
||||
const PreviewTile = ({ t, title, previewIcon, tags, defaultTagLabel }) => {
|
||||
const PreviewTile = ({ title, previewIcon, tags, defaultTagLabel }) => {
|
||||
return (
|
||||
<StyledPreviewTile>
|
||||
<div className="tile-header">
|
47
packages/components/ImageEditor/index.js
Normal file
47
packages/components/ImageEditor/index.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import Dropzone from "./Dropzone";
|
||||
import ImageCropper from "./ImageCropper";
|
||||
|
||||
const ImageEditor = ({
|
||||
t,
|
||||
image,
|
||||
onChangeImage,
|
||||
Preview,
|
||||
setPreview,
|
||||
isDisabled,
|
||||
classNameWrapperImageCropper,
|
||||
className,
|
||||
}) => {
|
||||
const setUploadedFile = (uploadedFile) =>
|
||||
onChangeImage({ ...image, uploadedFile });
|
||||
|
||||
const isDefaultAvatar =
|
||||
typeof image.uploadedFile === "string" &&
|
||||
image.uploadedFile.includes("default_user_photo");
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{image.uploadedFile && !isDefaultAvatar && (
|
||||
<div className={classNameWrapperImageCropper}>
|
||||
<ImageCropper
|
||||
t={t}
|
||||
image={image}
|
||||
onChangeImage={onChangeImage}
|
||||
uploadedFile={image.uploadedFile}
|
||||
setUploadedFile={setUploadedFile}
|
||||
setPreviewImage={setPreview}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
{Preview}
|
||||
</div>
|
||||
)}
|
||||
<Dropzone
|
||||
t={t}
|
||||
setUploadedFile={setUploadedFile}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageEditor;
|
Loading…
Reference in New Issue
Block a user