0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): image uploader (#3323)

This commit is contained in:
Xiao Yijun 2023-03-10 16:35:25 +08:00 committed by GitHub
parent 07fab67c24
commit 0971f99e98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 506 additions and 33 deletions

View file

@ -75,6 +75,7 @@
"react-dnd": "^16.0.0",
"react-dnd-html5-backend": "^16.0.0",
"react-dom": "^18.0.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.34.0",
"react-hot-toast": "^2.2.0",
"react-i18next": "^11.18.3",

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.25842 6.42508L9.16675 4.50841V12.5001C9.16675 12.7211 9.25455 12.9331 9.41083 13.0893C9.56711 13.2456 9.77907 13.3334 10.0001 13.3334C10.2211 13.3334 10.4331 13.2456 10.5893 13.0893C10.7456 12.9331 10.8334 12.7211 10.8334 12.5001V4.50841L12.7417 6.42508C12.8192 6.50318 12.9114 6.56518 13.0129 6.60749C13.1145 6.64979 13.2234 6.67158 13.3334 6.67158C13.4434 6.67158 13.5523 6.64979 13.6539 6.60749C13.7554 6.56518 13.8476 6.50318 13.9251 6.42508C14.0032 6.34761 14.0652 6.25544 14.1075 6.15389C14.1498 6.05234 14.1716 5.94342 14.1716 5.83341C14.1716 5.7234 14.1498 5.61448 14.1075 5.51293C14.0652 5.41138 14.0032 5.31921 13.9251 5.24174L10.5917 1.90841C10.5125 1.83254 10.419 1.77307 10.3167 1.73341C10.1139 1.65006 9.8863 1.65006 9.68342 1.73341C9.58112 1.77307 9.48767 1.83254 9.40841 1.90841L6.07508 5.24174C5.99738 5.31944 5.93575 5.41168 5.8937 5.5132C5.85165 5.61472 5.83001 5.72353 5.83001 5.83341C5.83001 5.94329 5.85165 6.0521 5.8937 6.15362C5.93575 6.25514 5.99738 6.34738 6.07508 6.42508C6.15278 6.50278 6.24502 6.56441 6.34654 6.60646C6.44806 6.64851 6.55687 6.67015 6.66675 6.67015C6.77663 6.67015 6.88544 6.64851 6.98696 6.60646C7.08847 6.56441 7.18072 6.50278 7.25842 6.42508ZM17.5001 11.6667C17.2791 11.6667 17.0671 11.7545 16.9108 11.9108C16.7545 12.0671 16.6667 12.2791 16.6667 12.5001V15.8334C16.6667 16.0544 16.579 16.2664 16.4227 16.4227C16.2664 16.5789 16.0544 16.6667 15.8334 16.6667H4.16675C3.94573 16.6667 3.73377 16.5789 3.57749 16.4227C3.42121 16.2664 3.33341 16.0544 3.33341 15.8334V12.5001C3.33341 12.2791 3.24562 12.0671 3.08934 11.9108C2.93306 11.7545 2.7211 11.6667 2.50008 11.6667C2.27907 11.6667 2.06711 11.7545 1.91083 11.9108C1.75455 12.0671 1.66675 12.2791 1.66675 12.5001V15.8334C1.66675 16.4965 1.93014 17.1323 2.39898 17.6012C2.86782 18.07 3.50371 18.3334 4.16675 18.3334H15.8334C16.4965 18.3334 17.1323 18.07 17.6012 17.6012C18.07 17.1323 18.3334 16.4965 18.3334 15.8334V12.5001C18.3334 12.2791 18.2456 12.0671 18.0893 11.9108C17.9331 11.7545 17.7211 11.6667 17.5001 11.6667Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,70 @@
@use '@/scss/underscore' as _;
.uploader {
border: 1px dashed var(--color-border);
border-radius: 8px;
padding: _.unit(5.5);
> input {
display: none;
}
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
.icon {
color: var(--color-text-secondary);
}
.uploadingIcon {
width: 20px;
height: 20px;
color: var(--color-primary);
}
.actionDescription {
margin-top: _.unit(1);
font: var(--font-body-2);
user-select: none;
}
}
&:hover {
cursor: pointer;
border-color: var(--color-primary);
.placeholder {
.icon {
color: var(--color-primary);
}
}
}
&.dragActive {
cursor: copy;
background-color: var(--color-hover-variant);
border-color: var(--color-primary);
.placeholder {
.icon {
color: var(--color-primary);
}
}
}
&.uploaderError {
border-color: var(--color-error);
}
}
.description {
font: var(--font-body-2);
color: var(--color-text-secondary);
margin-top: _.unit(2);
}
.error {
color: var(--color-error);
}

View file

@ -0,0 +1,122 @@
import type { UserAssetsResponse, AllowedUploadMimeType } from '@logto/schemas';
import { maxUploadFileSize } from '@logto/schemas';
import classNames from 'classnames';
import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import UploaderIcon from '@/assets/images/upload.svg';
import useApi from '@/hooks/use-api';
import { Ring } from '../Spinner';
import * as styles from './index.module.scss';
const allowedFileCount = 1;
type Props = {
maxSize: number; // In bytes
allowedMimeTypes: AllowedUploadMimeType[];
limitDescription: string;
onCompleted: (fileUrl: string) => void;
};
const FileUploader = ({ maxSize, allowedMimeTypes, limitDescription, onCompleted }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isUploading, setIsUploading] = useState(false);
const [uploaderError, setUploaderError] = useState<string>();
const hasError = Boolean(uploaderError);
const api = useApi();
const onDrop = useCallback(
async (acceptedFiles: File[]) => {
setUploaderError(undefined);
if (acceptedFiles.length > allowedFileCount) {
setUploaderError(t('components.uploader.error_file_count', { count: allowedFileCount }));
return;
}
const selectedFile = acceptedFiles[0];
if (!selectedFile) {
return;
}
if (!allowedMimeTypes.map(String).includes(selectedFile.type)) {
const supportedFileTypes = allowedMimeTypes.map((type) =>
type.split('/')[1]?.toUpperCase()
);
setUploaderError(t('components.uploader.error_file_type', { types: supportedFileTypes }));
return;
}
const fileSizeLimit = Math.min(maxSize, maxUploadFileSize);
if (selectedFile.size > fileSizeLimit) {
setUploaderError(t('components.uploader.error_file_size', { count: fileSizeLimit / 1024 }));
return;
}
const formData = new FormData();
formData.append('file', selectedFile);
try {
setIsUploading(true);
const { url } = await api
.post('api/user-assets', { body: formData })
.json<UserAssetsResponse>();
onCompleted(url);
} catch {
setUploaderError(t('components.uploader.error_upload'));
} finally {
setIsUploading(false);
}
},
[allowedMimeTypes, api, maxSize, onCompleted, t]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
disabled: isUploading,
});
return (
<div>
<div
{...getRootProps()}
className={classNames(
styles.uploader,
uploaderError && styles.uploaderError,
isDragActive && styles.dragActive
)}
>
<input {...getInputProps()} />
<div className={styles.placeholder}>
{isUploading ? (
<>
<Ring className={styles.uploadingIcon} />
<div className={styles.actionDescription}>{t('components.uploader.uploading')}</div>
</>
) : (
<>
<UploaderIcon className={styles.icon} />
<div className={styles.actionDescription}>
{t('components.uploader.action_description')}
</div>
</>
)}
</div>
</div>
<div className={classNames(styles.description, hasError && styles.error)}>
{hasError ? uploaderError : limitDescription}
</div>
</div>
);
};
export default FileUploader;

View file

@ -0,0 +1,28 @@
@use '@/scss/underscore' as _;
.imageUploader {
border: 1px dashed var(--color-border);
border-radius: 8px;
padding: _.unit(6) 0;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.delete {
position: absolute;
right: _.unit(2);
bottom: _.unit(2);
}
> img {
height: 40px;
object-fit: cover;
}
}
.limit {
margin-top: _.unit(2);
font: var(--font-body-2);
color: var(--color-text-secondary);
}

View file

@ -0,0 +1,50 @@
import { useTranslation } from 'react-i18next';
import Delete from '@/assets/images/delete.svg';
import FileUploader from '../FileUploader';
import IconButton from '../IconButton';
import * as styles from './index.module.scss';
type Props = {
name: string;
value: string;
onChange: (value: string) => void;
};
const maxImageSize = 500 * 1024; // 500 KB
const ImageUploader = ({ name, value, onChange }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const limitDescription = t('components.uploader.image_limit');
if (!value) {
return (
<FileUploader
allowedMimeTypes={['image/jpeg', 'image/png', 'image/gif', 'image/webp']}
maxSize={maxImageSize}
limitDescription={limitDescription}
onCompleted={onChange}
/>
);
}
return (
<div>
<div className={styles.imageUploader}>
<img alt={name} src={value} />
<IconButton
className={styles.delete}
onClick={() => {
onChange('');
}}
>
<Delete />
</IconButton>
</div>
<div className={styles.limit}>{limitDescription}</div>
</div>
);
};
export default ImageUploader;

View file

@ -1,6 +1,13 @@
import { readFile } from 'fs/promises';
import { generateStandardId } from '@logto/core-kit';
import type { UserAssetsResponse } from '@logto/schemas';
import {
userAssetsGuard,
userAssetsServiceStatusGuard,
allowUploadMimeTypes,
maxUploadFileSize,
} from '@logto/schemas';
import { format } from 'date-fns';
import { object } from 'zod';
@ -8,21 +15,21 @@ import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import SystemContext from '#src/tenants/SystemContext.js';
import assertThat from '#src/utils/assert-that.js';
import {
allowUploadMimeTypes,
maxUploadFileSize,
uploadFileGuard,
} from '#src/utils/storage/consts.js';
import { uploadFileGuard } from '#src/utils/storage/consts.js';
import { buildUploadFile } from '#src/utils/storage/index.js';
import { getTenantId } from '#src/utils/tenant.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function userAssetsRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get('/user-assets/service-status', async (ctx, next) => {
router.get(
'/user-assets/service-status',
koaGuard({
response: userAssetsServiceStatusGuard,
}),
async (ctx, next) => {
const { storageProviderConfig } = SystemContext.shared;
ctx.body = storageProviderConfig
const status = storageProviderConfig
? {
status: 'ready',
allowUploadMimeTypes,
@ -32,8 +39,11 @@ export default function userAssetsRoutes<T extends AuthedRouter>(...[router]: Ro
status: 'not_configured',
};
ctx.body = status;
return next();
});
}
);
router.post(
'/user-assets',
@ -41,12 +51,16 @@ export default function userAssetsRoutes<T extends AuthedRouter>(...[router]: Ro
files: object({
file: uploadFileGuard,
}),
response: userAssetsGuard,
}),
async (ctx, next) => {
const { file } = ctx.guard.files;
assertThat(file.size <= maxUploadFileSize, 'guard.file_size_exceeded');
assertThat(allowUploadMimeTypes.includes(file.mimetype), 'guard.mime_type_not_allowed');
assertThat(
allowUploadMimeTypes.map(String).includes(file.mimetype),
'guard.mime_type_not_allowed'
);
const tenantId = getTenantId(ctx.URL);
assertThat(tenantId, 'guard.can_not_get_tenant_id');
@ -67,9 +81,11 @@ export default function userAssetsRoutes<T extends AuthedRouter>(...[router]: Ro
publicUrl: storageProviderConfig.publicUrl,
});
ctx.body = {
const result: UserAssetsResponse = {
url,
};
ctx.body = result;
} catch {
throw new RequestError('storage.upload_error');
}

View file

@ -1,18 +1,5 @@
import { number, object, string } from 'zod';
export const maxUploadFileSize = 8 * 1024 * 1024; // 8MB
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
export const allowUploadMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/vnd.microsoft.icon',
'image/svg+xml',
'image/tiff',
'image/webp',
'image/bmp',
];
export const uploadFileGuard = object({
filepath: string(),
mimetype: string(),

View file

@ -0,0 +1,15 @@
const components = {
uploader: {
action_description: 'Drag and drop or browse', // UNTRANSLATED
uploading: 'Uploading...', // UNTRANSLATED
image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED
error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED
error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED
error_file_type:
'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED
error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED
error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED
},
};
export default components;

View file

@ -3,6 +3,7 @@ import api_resources from './api-resources.js';
import application_details from './application-details.js';
import applications from './applications.js';
import cloud from './cloud.js';
import components from './components.js';
import connector_details from './connector-details.js';
import connectors from './connectors.js';
import contact from './contact.js';
@ -55,6 +56,7 @@ const admin_console = {
permissions,
cloud,
profile,
components,
};
export default admin_console;

View file

@ -0,0 +1,15 @@
const components = {
uploader: {
action_description: 'Drag and drop or browse', // UNTRANSLATED
uploading: 'Uploading...', // UNTRANSLATED
image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED
error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED
error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED
error_file_type:
'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED
error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED
error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED
},
};
export default components;

View file

@ -3,6 +3,7 @@ import api_resources from './api-resources.js';
import application_details from './application-details.js';
import applications from './applications.js';
import cloud from './cloud.js';
import components from './components.js';
import connector_details from './connector-details.js';
import connectors from './connectors.js';
import contact from './contact.js';
@ -55,6 +56,7 @@ const admin_console = {
permissions,
cloud,
profile,
components,
};
export default admin_console;

View file

@ -0,0 +1,15 @@
const components = {
uploader: {
action_description: 'Drag and drop or browse', // UNTRANSLATED
uploading: 'Uploading...', // UNTRANSLATED
image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED
error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED
error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED
error_file_type:
'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED
error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED
error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED
},
};
export default components;

View file

@ -3,6 +3,7 @@ import api_resources from './api-resources.js';
import application_details from './application-details.js';
import applications from './applications.js';
import cloud from './cloud.js';
import components from './components.js';
import connector_details from './connector-details.js';
import connectors from './connectors.js';
import contact from './contact.js';
@ -55,6 +56,7 @@ const admin_console = {
permissions,
cloud,
profile,
components,
};
export default admin_console;

View file

@ -0,0 +1,15 @@
const components = {
uploader: {
action_description: 'Drag and drop or browse', // UNTRANSLATED
uploading: 'Uploading...', // UNTRANSLATED
image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED
error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED
error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED
error_file_type:
'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED
error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED
error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED
},
};
export default components;

View file

@ -3,6 +3,7 @@ import api_resources from './api-resources.js';
import application_details from './application-details.js';
import applications from './applications.js';
import cloud from './cloud.js';
import components from './components.js';
import connector_details from './connector-details.js';
import connectors from './connectors.js';
import contact from './contact.js';
@ -55,6 +56,7 @@ const admin_console = {
permissions,
cloud,
profile,
components,
};
export default admin_console;

View file

@ -0,0 +1,15 @@
const components = {
uploader: {
action_description: 'Drag and drop or browse', // UNTRANSLATED
uploading: 'Uploading...', // UNTRANSLATED
image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED
error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED
error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED
error_file_type:
'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED
error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED
error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED
},
};
export default components;

View file

@ -3,6 +3,7 @@ import api_resources from './api-resources.js';
import application_details from './application-details.js';
import applications from './applications.js';
import cloud from './cloud.js';
import components from './components.js';
import connector_details from './connector-details.js';
import connectors from './connectors.js';
import contact from './contact.js';
@ -55,6 +56,7 @@ const admin_console = {
permissions,
cloud,
profile,
components,
};
export default admin_console;

View file

@ -0,0 +1,15 @@
const components = {
uploader: {
action_description: 'Drag and drop or browse', // UNTRANSLATED
uploading: 'Uploading...', // UNTRANSLATED
image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED
error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED
error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED
error_file_type:
'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED
error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED
error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED
},
};
export default components;

View file

@ -3,6 +3,7 @@ import api_resources from './api-resources.js';
import application_details from './application-details.js';
import applications from './applications.js';
import cloud from './cloud.js';
import components from './components.js';
import connector_details from './connector-details.js';
import connectors from './connectors.js';
import contact from './contact.js';
@ -55,6 +56,7 @@ const admin_console = {
permissions,
cloud,
profile,
components,
};
export default admin_console;

View file

@ -0,0 +1,15 @@
const components = {
uploader: {
action_description: 'Drag and drop or browse', // UNTRANSLATED
uploading: 'Uploading...', // UNTRANSLATED
image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED
error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED
error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED
error_file_type:
'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED
error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED
error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED
},
};
export default components;

View file

@ -3,6 +3,7 @@ import api_resources from './api-resources.js';
import application_details from './application-details.js';
import applications from './applications.js';
import cloud from './cloud.js';
import components from './components.js';
import connector_details from './connector-details.js';
import connectors from './connectors.js';
import contact from './contact.js';
@ -55,6 +56,7 @@ const admin_console = {
permissions,
cloud,
profile,
components,
};
export default admin_console;

View file

@ -0,0 +1,15 @@
const components = {
uploader: {
action_description: 'Drag and drop or browse', // UNTRANSLATED
uploading: 'Uploading...', // UNTRANSLATED
image_limit: 'Upload image under 500KB, JPG, PNG, GIF, WEBP only.', // UNTRANSLATED
error_upload: 'Something went wrong. File upload failed.', // UNTRANSLATED
error_file_size: 'File size is too large. Please upload a file under {{count, number}}KB.', // UNTRANSLATED
error_file_type:
'File type is not supported. {{types, list(style: narrow; type: conjunction;)}} only.', // UNTRANSLATED
error_file_count_one: 'You can only upload {{count, number}} file.', // UNTRANSLATED
error_file_count_other: 'You can only upload {{count, number}} files.', // UNTRANSLATED
},
};
export default components;

View file

@ -3,6 +3,7 @@ import api_resources from './api-resources.js';
import application_details from './application-details.js';
import applications from './applications.js';
import cloud from './cloud.js';
import components from './components.js';
import connector_details from './connector-details.js';
import connectors from './connectors.js';
import contact from './contact.js';
@ -55,6 +56,7 @@ const admin_console = {
permissions,
cloud,
profile,
components,
};
export default admin_console;

View file

@ -12,3 +12,4 @@ export * from './verification-code.js';
export * from './application.js';
export * from './system.js';
export * from './tenant.js';
export * from './user-assets.js';

View file

@ -0,0 +1,33 @@
import { z } from 'zod';
export const maxUploadFileSize = 8 * 1024 * 1024; // 8MB
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
export const allowUploadMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/vnd.microsoft.icon',
'image/svg+xml',
'image/tiff',
'image/webp',
'image/bmp',
] as const;
const allowUploadMimeTypeGuard = z.enum(allowUploadMimeTypes);
export type AllowedUploadMimeType = z.infer<typeof allowUploadMimeTypeGuard>;
export const userAssetsServiceStatusGuard = z.object({
status: z.union([z.literal('ready'), z.literal('not_configured')]),
allowUploadMimeTypes: z.array(allowUploadMimeTypeGuard).optional(),
maxUploadFileSize: z.number().optional(),
});
export type UserAssetsServiceStatusResponse = z.infer<typeof userAssetsServiceStatusGuard>;
export const userAssetsGuard = z.object({
url: z.string(),
});
export type UserAssetsResponse = z.infer<typeof userAssetsGuard>;

View file

@ -228,6 +228,7 @@ importers:
react-dnd: ^16.0.0
react-dnd-html5-backend: ^16.0.0
react-dom: ^18.0.0
react-dropzone: ^14.2.3
react-hook-form: ^7.34.0
react-hot-toast: ^2.2.0
react-i18next: ^11.18.3
@ -302,6 +303,7 @@ importers:
react-dnd: 16.0.0_3hx2ussxxho4jajbwrd6gq34qe
react-dnd-html5-backend: 16.0.0
react-dom: 18.2.0_react@18.2.0
react-dropzone: 14.2.3_react@18.2.0
react-hook-form: 7.34.0_react@18.2.0
react-hot-toast: 2.2.0_npw22p3c4ehm7n7vxn2gqac44u
react-i18next: 11.18.3_shxxmfhtk2bc4pbx5cyq3uoph4
@ -4882,6 +4884,11 @@ packages:
/asynckit/0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
/attr-accept/2.2.2:
resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==}
engines: {node: '>=4'}
dev: true
/available-typed-arrays/1.0.5:
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
engines: {node: '>= 0.4'}
@ -7128,6 +7135,13 @@ packages:
flat-cache: 3.0.4
dev: true
/file-selector/0.6.0:
resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==}
engines: {node: '>= 12'}
dependencies:
tslib: 2.4.1
dev: true
/filelist/1.0.2:
resolution: {integrity: sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==}
dependencies:
@ -12074,6 +12088,18 @@ packages:
scheduler: 0.23.0
dev: true
/react-dropzone/14.2.3_react@18.2.0:
resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==}
engines: {node: '>= 10.13'}
peerDependencies:
react: '>= 16.8 || 18.0.0 || ^18.0.0'
dependencies:
attr-accept: 2.2.2
file-selector: 0.6.0
prop-types: 15.8.1
react: 18.2.0
dev: true
/react-error-overlay/6.0.9:
resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==}
dev: true