Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: componentize the UploadAvatarFieldWithCrop #953

Merged
merged 1 commit into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";

Check warning on line 3 in packages/toolkit/src/components/UploadAvatarFieldWithCrop.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'GeneralUseFormReturn' is defined but never used

Check warning on line 3 in packages/toolkit/src/components/UploadAvatarFieldWithCrop.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'GeneralUseFormReturn' is defined but never used
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>;

Check warning on line 14 in packages/toolkit/src/components/UploadAvatarFieldWithCrop.tsx

View workflow job for this annotation

GitHub Actions / unit-test

Unexpected any. Specify a different type

Check warning on line 14 in packages/toolkit/src/components/UploadAvatarFieldWithCrop.tsx

View workflow job for this annotation

GitHub Actions / unit-test

Unexpected any. Specify a different type
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>
);
};
Loading