Skip to content

Commit

Permalink
feat: Add FieldUploadPreview component on AccountProfileForm
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabien Essid committed Dec 21, 2023
1 parent f050519 commit 82df320
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 20 deletions.
116 changes: 116 additions & 0 deletions src/components/FieldUpload/FieldUploadPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { FC, useCallback, useEffect, useState } from 'react';

import { Box, Flex, FlexProps, IconButton } from '@chakra-ui/react';
import { useFormContext, useFormFields } from '@formiz/core';
import { LuX } from 'react-icons/lu';

import { FieldUploadValue } from '@/components/FieldUpload';

const ImagePreview = ({
image,
onClick,
}: {
image: string;
onClick: React.MouseEventHandler<HTMLButtonElement>;
}) => {
return (
<Box
position="relative"
bg="gray.200"
height="100%"
aspectRatio="1"
overflow="hidden"
rounded="md"
>
<Box as="img" width="100%" height="100%" objectFit="cover" src={image} />
<IconButton
position="absolute"
top="1"
right="1"
icon={<LuX fontSize="sm" />}
aria-label="Remove"
rounded="full"
minWidth="6"
minHeight="6"
width="6"
height="6"
onClick={onClick}
/>
</Box>
);
};

export type FieldUploadPreviewProps = FlexProps & {
uploaderName: string;
};

export const FieldUploadPreview: FC<
React.PropsWithChildren<FieldUploadPreviewProps>
> = ({ uploaderName, ...rest }) => {
const fields = useFormFields({
fields: [uploaderName] as const, // To get the uploader values
});
const form = useFormContext();

const [fileToPreview, setFileToPreview] = useState<string>();

const previewFile = useCallback(async () => {
const uploaderFieldValue = fields?.[uploaderName]
?.value as FieldUploadValue;

if (!uploaderFieldValue || !uploaderFieldValue?.name) {
setFileToPreview(undefined);
return;
}

const hasUserUploadedAFile = uploaderFieldValue.file;
const hasDefaultFileSet = uploaderFieldValue.name && !hasUserUploadedAFile;

if (hasDefaultFileSet) {
setFileToPreview(uploaderFieldValue.name);
return;
}

const uploadedFileToPreview = await new Promise<string>(
(resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result?.toString() ?? '');
reader.onerror = reject;
if (uploaderFieldValue.file) {
reader.readAsDataURL(uploaderFieldValue.file);
}
}
);

setFileToPreview(uploadedFileToPreview);
}, [fields, uploaderName]);

useEffect(() => {
previewFile();
}, [previewFile]);

return (
fileToPreview && (
<Flex
mt={2}
background="white"
height="32"
width="full"
rounded="md"
boxShadow="sm"
border="1px solid"
borderColor="gray.200"
alignItems="center"
p={4}
{...rest}
>
<ImagePreview
image={fileToPreview}
onClick={() => {
form.setValues({ [uploaderName]: null });
}}
/>
</Flex>
)
);
};
27 changes: 27 additions & 0 deletions src/components/FieldUpload/docs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Box, Button, Stack } from '@chakra-ui/react';
import { Formiz, useForm } from '@formiz/core';
import { Meta } from '@storybook/react';

import { FieldUploadPreview } from '@/components/FieldUpload/FieldUploadPreview';
import { useFieldUploadFileFromUrl } from '@/components/FieldUpload/utils';

import { FieldUpload } from '.';
Expand Down Expand Up @@ -50,3 +51,29 @@ export const DefaultValue = () => {
</Formiz>
);
};

export const WithPreview = () => {
const initialFiles = useFieldUploadFileFromUrl(
'https://plus.unsplash.com/premium_photo-1674593231084-d8b27596b134?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwyfHx8ZW58MHx8fHx8'
);

const form = useForm({
ready: initialFiles.isSuccess,
initialValues: {
file: initialFiles.data,
},
onSubmit: console.log,
});

return (
<Formiz connect={form} autoForm>
<Stack spacing={4}>
<FieldUpload name="file" label="File" />
<FieldUploadPreview uploaderName="file" />
<Box>
<Button type="submit">Submit</Button>
</Box>
</Stack>
</Formiz>
);
};
22 changes: 11 additions & 11 deletions src/components/FieldUpload/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ import { FiPaperclip } from 'react-icons/fi';
import { FormGroup, FormGroupProps } from '@/components/FormGroup';

export type FieldUploadValue = {
file?: File;
lastModified?: number;
lastModifiedDate?: Date;
name: string;
size?: number;
type?: string;
lastModified?: number;
lastModifiedDate?: Date;
file: File;
};

export type FieldUploadProps<FormattedValue = FieldUploadValue> = FieldProps<
FieldUploadValue,
FormattedValue
> &
FormGroupProps;
FormGroupProps & {
inputText?: string;
};

export const FieldUpload = <FormattedValue = FieldUploadValue,>(
props: FieldUploadProps<FormattedValue>
Expand All @@ -29,9 +31,8 @@ export const FieldUpload = <FormattedValue = FieldUploadValue,>(
id,
isRequired,
setValue,
value,
shouldDisplayError,
otherProps: { children, label, helper, ...rest },
otherProps: { children, label, helper, inputText, ...rest },
} = useField(props);

const formGroupProps = {
Expand All @@ -44,7 +45,7 @@ export const FieldUpload = <FormattedValue = FieldUploadValue,>(
...rest,
};

const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const handleChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
const file = target.files?.[0];

if (!file) {
Expand Down Expand Up @@ -82,11 +83,10 @@ export const FieldUpload = <FormattedValue = FieldUploadValue,>(
id={id}
onChange={handleChange}
/>
<Icon as={FiPaperclip} mr="2" />{' '}
{
value?.name || 'Select file' // TODO translations
}
<Icon as={FiPaperclip} mr="2" />
{inputText}
</Input>

{children}
</FormGroup>
);
Expand Down
30 changes: 22 additions & 8 deletions src/features/account/AccountProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ErrorPage } from '@/components/ErrorPage';
import { FieldInput } from '@/components/FieldInput';
import { FieldSelect } from '@/components/FieldSelect';
import { FieldUpload, FieldUploadValue } from '@/components/FieldUpload';
import { FieldUploadPreview } from '@/components/FieldUpload/FieldUploadPreview';
import { LoaderFull } from '@/components/LoaderFull';
import { useToastError, useToastSuccess } from '@/components/Toast';
import { useAvatarUpload } from '@/features/account/useAvatarUpload';
Expand Down Expand Up @@ -52,16 +53,23 @@ export const AccountProfileForm = () => {
initialValues: {
name: account.data?.name ?? undefined,
language: account.data?.language ?? undefined,
image: { name: account.data?.image ?? '' } ?? undefined,
},
onValidSubmit: async ({ image, ...values }) => {
try {
const { fileUrl } = await uploadFile.mutateAsync({
contentType: image.type ?? '',
file: image.file,
});
updateAccount.mutate({ ...values, image: fileUrl });
if (image?.file) {
const { fileUrl } = await uploadFile.mutateAsync({
contentType: image.type ?? '',
file: image?.file,
});
updateAccount.mutate({ ...values, image: fileUrl });
} else {
updateAccount.mutate(values);
}
} catch {
form.setErrors({ image: 'Upload fail' }); // TODO translations
form.setErrors({
image: t('account:profile.feedbacks.uploadError.title'),
});
}
},
});
Expand All @@ -77,8 +85,14 @@ export const AccountProfileForm = () => {
<Stack spacing={4}>
<FieldUpload
name="image"
label="Avatar" // TODO: translations
required
label={t('account:data.avatar.label')}
inputText={t('account:data.avatar.inputText')}
required={t('account:data.avatar.required')}
/>
<FieldUploadPreview
uploaderName="image"
width="fit-content"
p="0"
/>
<FieldInput
name="name"
Expand Down
8 changes: 8 additions & 0 deletions src/locales/en/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
},
"updateError": {
"title": "Update failed"
},
"uploadError": {
"title": "Upload failed"
}
},
"actions": {
Expand Down Expand Up @@ -53,6 +56,11 @@
}
},
"data": {
"avatar": {
"label": "Avatar",
"inputText": "Update avatar...",
"required": "Avatar is required"
},
"name": {
"label": "Name",
"required": "Name is required"
Expand Down
8 changes: 8 additions & 0 deletions src/locales/fr/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
},
"updateError": {
"title": "Échec de la mise à jour"
},
"uploadError": {
"title": "Échec de l'import"
}
},
"actions": {
Expand All @@ -19,6 +22,11 @@
"title": "Informations du profil"
},
"data": {
"avatar": {
"label": "Avatar",
"inputText": "Modifier l'avatar...",
"required": "L'avatar est requis"
},
"name": {
"label": "Nom",
"required": "Le nom est requis"
Expand Down
2 changes: 1 addition & 1 deletion src/server/routers/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const accountRouter = createTRPCRouter({
},
})
.input(
zUserAccount().required().pick({
zUserAccount().pick({
image: true,
name: true,
language: true,
Expand Down

0 comments on commit 82df320

Please sign in to comment.