Skip to content

Commit

Permalink
feat: componentize the UploadAvatarFieldWithCrop (#953)
Browse files Browse the repository at this point in the history
Because

- We will use UploadAvatarFieldWithCrop for user/organization avatar
uploader, this component need to be unified

This commit

- componentize the UploadAvatarFieldWithCrop
  • Loading branch information
EiffelFly committed Feb 14, 2024
1 parent 8e2d89f commit 62cbb1b
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 126 deletions.
2 changes: 1 addition & 1 deletion packages/toolkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@instill-ai/toolkit",
"version": "0.80.4-rc.2",
"version": "0.80.4-rc.5",
"description": "Instill AI's frontend toolkit",
"repository": "https://github.com/instill-ai/design-system.git",
"bugs": "https://github.com/instill-ai/design-system/issues",
Expand Down
141 changes: 141 additions & 0 deletions packages/toolkit/src/components/UploadAvatarFieldWithCrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as React from "react";
import { Button, Dialog, Form, Input } from "@instill-ai/design-system";
import { GeneralUseFormReturn } from "../lib";
import { FormLabel } from "../view/settings/FormLabel";
import AvatarEditor from "react-avatar-editor";
import { UseFormReturn } from "react-hook-form";

export const UploadAvatarFieldWithCrop = ({
fieldName,
form,
placeholder,
}: {
fieldName: string;
form: UseFormReturn<any>;
placeholder?: React.ReactNode;
}) => {
const [profileAvatar, setProfileAvatar] = React.useState<
string | File | null
>(null);
const [openProfileAvatar, setOpenProfileAvatar] =
React.useState<boolean>(false);
const editorRef = React.useRef<AvatarEditor>(null);

function handleSetProfilePicture() {
// Save the cropped image as a file or perform any other actions here
// For simplicity, let's assume you have a function to handle image upload
const croppedImage = editorRef.current
?.getImageScaledToCanvas()
?.toDataURL();
setProfileAvatar(croppedImage ?? null);
form.setValue(fieldName, croppedImage ?? undefined);
// Close the dialog or perform any other actions
setOpenProfileAvatar(false);
}

function handleCancelCropProfile() {
setOpenProfileAvatar(false);
setProfileAvatar(null);

form.resetField("profile.avatar");
}

return (
<React.Fragment>
<Form.Field
control={form.control}
name={fieldName}
render={({ field }) => {
return (
<Form.Item className="w-full">
<FormLabel title="Upload your profile" optional={true} />
<Form.Control>
<div>
<label
htmlFor="upload-avatar-field"
className="flex h-[150px] w-full cursor-pointer flex-col items-center justify-center rounded border border-dashed border-semantic-bg-line bg-semantic-bg-base-bg text-semantic-fg-secondary product-body-text-3-medium"
>
{field.value ? (
<img
src={
profileAvatar ? String(profileAvatar) : field.value
}
alt="avatar-profile"
className="h-[150px] rounded-full object-contain"
/>
) : placeholder ? (
placeholder
) : (
<p>Upload your avatar</p>
)}

<Input.Root className="hidden">
<Input.Core
{...field}
id="upload-avatar-field"
type="file"
accept="images/*"
value={undefined}
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
field.onChange(result);
setProfileAvatar(String(result));
};
reader.readAsDataURL(file);
setOpenProfileAvatar(true);
}
}}
/>
</Input.Root>
</label>
</div>
</Form.Control>
<Form.Message />
</Form.Item>
);
}}
/>
<Dialog.Root open={openProfileAvatar}>
<Dialog.Content className="!w-[400px]">
<Dialog.Header>
<Dialog.Title>Crop your image</Dialog.Title>
</Dialog.Header>
<div className="flex items-center justify-center">
{profileAvatar ? (
<AvatarEditor
ref={editorRef}
image={profileAvatar}
width={300}
height={300}
border={20}
borderRadius={9999}
color={[248, 249, 252]}
scale={1}
/>
) : null}
</div>
<div className="flex flex-row justify-between gap-x-4">
<Button
onClick={() => handleSetProfilePicture()}
variant="primary"
className="w-full"
>
Set
</Button>
<Button
onClick={() => handleCancelCropProfile()}
variant="secondaryGrey"
className="w-full"
>
Cancel
</Button>
</div>
</Dialog.Content>
</Dialog.Root>
</React.Fragment>
);
};
1 change: 1 addition & 0 deletions packages/toolkit/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ export * from "./ReferenceHintTag";
export * from "./ReferenceHintDataTypeTag";
export * from "./cells";
export * from "./TableError";
export * from "./UploadAvatarFieldWithCrop";
export * from "./UserProfileCard";
export * from "./WarnUnsavedChangesDialog";
130 changes: 5 additions & 125 deletions packages/toolkit/src/view/settings/user/UserProfileTab.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as z from "zod";
import * as React from "react";

import { Setting, instillUserRoles } from "..";
import { Setting } from "..";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Button,
Dialog,
Form,
Input,
Switch,
Expand All @@ -24,8 +23,7 @@ import {
useAuthenticatedUser,
} from "../../../lib";
import { FormLabel } from "../FormLabel";
import { LoadingSpin } from "../../../components";
import AvatarEditor from "react-avatar-editor";
import { LoadingSpin, UploadAvatarFieldWithCrop } from "../../../components";
import { useUpdateAuthenticatedUser } from "../../../lib";

export const UserProfileTabSchema = z.object({
Expand Down Expand Up @@ -59,13 +57,6 @@ export const UserProfileTab = () => {
const { amplitudeIsInit } = useAmplitudeCtx();
const { accessToken, enabledQuery } = useInstillStore(useShallow(selector));
const { toast } = useToast();
const [profileAvatar, setProfileAvatar] = React.useState<
string | File | null
>(null);
const [isOpenProfileAvatar, setIsOpenProfileAvatar] =
React.useState<boolean>(false);

const editorRef = React.useRef<AvatarEditor>(null);

const me = useAuthenticatedUser({
accessToken,
Expand Down Expand Up @@ -117,24 +108,6 @@ export const UserProfileTab = () => {
}
}

function handleSetProfilePicture() {
// Save the cropped image as a file or perform any other actions here
// For simplicity, let's assume you have a function to handle image upload
const croppedImage = editorRef.current
?.getImageScaledToCanvas()
?.toDataURL();
setProfileAvatar(croppedImage ?? null);
// Close the dialog or perform any other actions
setIsOpenProfileAvatar(false);
}

function handleCancelCropProfile() {
setIsOpenProfileAvatar(false);
setProfileAvatar(null);

form.resetField("profile.avatar");
}

return (
<Setting.TabRoot>
<Setting.TabHeader
Expand Down Expand Up @@ -277,65 +250,9 @@ export const UserProfileTab = () => {
description="This will be displayed on your profile."
/>
<Setting.TabSectionContent className="gap-y-4">
<Form.Field
control={form.control}
name="profile.avatar"
render={({ field }) => {
return (
<Form.Item className="w-full">
<FormLabel
title="Upload your profile test"
optional={true}
/>
<Form.Control>
<div>
<label
htmlFor="org-logo-input"
className="flex h-[150px] w-full cursor-pointer flex-col items-center justify-center rounded border border-dashed border-semantic-bg-line bg-semantic-bg-base-bg text-semantic-fg-secondary product-body-text-3-medium"
>
{field.value ? (
<img
src={
profileAvatar
? String(profileAvatar)
: field.value
}
alt={`${me.data?.name}-profile`}
className="h-[150px] rounded-full object-contain"
/>
) : (
<p>Upload your profile test</p>
)}

<Input.Root className="hidden">
<Input.Core
{...field}
id="org-logo-input"
type="file"
accept="images/*"
value={undefined}
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
field.onChange(result);
setProfileAvatar(String(result));
};
reader.readAsDataURL(file);
setIsOpenProfileAvatar(true);
}
}}
/>
</Input.Root>
</label>
</div>
</Form.Control>
<Form.Message />
</Form.Item>
);
}}
<UploadAvatarFieldWithCrop
fieldName="profile.avatar"
form={form}
/>
</Setting.TabSectionContent>
</Setting.TabSectionRoot>
Expand Down Expand Up @@ -444,43 +361,6 @@ export const UserProfileTab = () => {
</div>
</form>
</Form.Root>
<Dialog.Root open={isOpenProfileAvatar}>
<Dialog.Content className="!w-[400px]">
<Dialog.Header>
<Dialog.Title>Crop your new profile picture</Dialog.Title>
</Dialog.Header>
<div className="flex items-center justify-center">
{profileAvatar ? (
<AvatarEditor
ref={editorRef}
image={profileAvatar}
width={300}
height={300}
border={20}
borderRadius={9999}
color={[248, 249, 252]}
scale={1}
/>
) : null}
</div>
<div className="flex flex-row justify-between gap-x-4">
<Button
onClick={() => handleSetProfilePicture()}
variant="primary"
className="w-full"
>
Set new profile picture
</Button>
<Button
onClick={() => handleCancelCropProfile()}
variant="secondaryGrey"
className="w-full"
>
Cancel
</Button>
</div>
</Dialog.Content>
</Dialog.Root>
</Setting.TabRoot>
);
};

0 comments on commit 62cbb1b

Please sign in to comment.