import React, { cloneElement, ComponentPropsWithRef, Fragment, JSXElementConstructor, ReactElement, ReactNode, useCallback, useId, useRef, } from 'react'; import clsx from 'clsx'; import {Button} from '../buttons/button'; import {Trans} from '../../i18n/trans'; import {useActiveUpload} from '../../uploads/uploader/use-active-upload'; import {UploadInputType} from '../../uploads/types/upload-input-config'; import {useController} from 'react-hook-form'; import {mergeProps} from '@react-aria/utils'; import {ProgressBar} from '../progress/progress-bar'; import {Disk} from '../../uploads/types/backend-metadata'; import {toast} from '@common/ui/toast/toast'; import {Field} from '@common/ui/forms/input-field/field'; import { getInputFieldClassNames, InputFieldStyle, } from '@common/ui/forms/input-field/get-input-field-class-names'; import {FileEntry} from '@common/uploads/file-entry'; import {useAutoFocus} from '@common/ui/focus/use-auto-focus'; import {UploadStrategyConfig} from '@common/uploads/uploader/strategy/upload-strategy'; import {SvgIconProps} from '@common/icons/svg-icon'; import {IconButton} from '@common/ui/buttons/icon-button'; import {AddAPhotoIcon} from '@common/icons/material/AddAPhoto'; import {AvatarPlaceholderIcon} from '@common/auth/ui/account-settings/avatar/avatar-placeholder-icon'; import {ButtonBaseProps} from '@common/ui/buttons/button-base'; const TwoMB = 2 * 1024 * 1024; interface ImageSelectorProps { className?: string; label?: ReactNode; description?: ReactNode; invalid?: boolean; errorMessage?: ReactNode; required?: boolean; disabled?: boolean; value?: string; onChange?: (newValue: string) => void; defaultValue?: string; diskPrefix: string; showRemoveButton?: boolean; showEditButtonOnHover?: boolean; autoFocus?: boolean; variant?: 'input' | 'square' | 'avatar'; placeholderIcon?: ReactElement; previewSize?: string; previewRadius?: string; stretchPreview?: boolean; } export function ImageSelector({ className, label, description, value, onChange, defaultValue, diskPrefix, showRemoveButton, showEditButtonOnHover = false, invalid, errorMessage, required, autoFocus, variant = 'input', previewSize = 'h-80', placeholderIcon, stretchPreview = false, previewRadius, disabled, }: ImageSelectorProps) { const { uploadFile, entry, uploadStatus, deleteEntry, isDeletingEntry, percentage, } = useActiveUpload(); const inputRef = useRef(null); useAutoFocus({autoFocus}, inputRef); const fieldId = useId(); const labelId = label ? `${fieldId}-label` : undefined; const descriptionId = description ? `${fieldId}-description` : undefined; const imageUrl = value || entry?.url; const uploadOptions: UploadStrategyConfig = { showToastOnRestrictionFail: true, restrictions: { allowedFileTypes: [UploadInputType.image], maxFileSize: TwoMB, }, metadata: { diskPrefix, disk: Disk.public, }, onSuccess: (entry: FileEntry) => { onChange?.(entry.url); }, onError: message => { if (message) { toast.danger(message); } }, }; const inputFieldClassNames = getInputFieldClassNames({ description, descriptionPosition: 'top', invalid, }); let VariantElement: JSXElementConstructor; if (variant === 'avatar') { VariantElement = AvatarVariant; } else if (variant === 'square') { VariantElement = SquareVariant; } else { VariantElement = InputVariant; } const removeButton = showRemoveButton ? ( ) : null; const useDefaultButton = defaultValue != null && value !== defaultValue ? ( ) : null; const handleUpload = useCallback(() => { inputRef.current?.click(); }, []); return (
{label && (
{label}
)} {description && (
{description}
)}
{ if (e.target.files?.length) { uploadFile(e.target.files[0], uploadOptions); } }} /> {uploadStatus === 'inProgress' && ( )}
); } interface VariantProps { children: ReactElement>; inputFieldClassNames: InputFieldStyle; previewSize?: ImageSelectorProps['previewSize']; placeholderIcon?: ImageSelectorProps['placeholderIcon']; isLoading?: boolean; imageUrl?: string; removeButton?: ReactElement | null; useDefaultButton?: ReactElement | null; showEditButtonOnHover?: boolean; stretchPreview?: boolean; previewRadius?: string; handleUpload: () => void; disabled?: boolean; } function InputVariant({ children, inputFieldClassNames, imageUrl, previewSize, stretchPreview, isLoading, handleUpload, removeButton, useDefaultButton, disabled, }: VariantProps) { if (imageUrl) { return (
handleUpload()} src={imageUrl} alt="" /> {children}
{removeButton && cloneElement(removeButton, {variant: 'outline'})} {useDefaultButton && cloneElement(useDefaultButton, {variant: 'outline'})}
); } return cloneElement(children, { className: clsx( inputFieldClassNames.input, 'py-8', 'file:bg-primary file:text-on-primary file:border-none file:rounded file:text-sm file:font-semibold file:px-10 file:h-24 file:mr-10', ), }); } function SquareVariant({ children, placeholderIcon, previewSize, imageUrl, stretchPreview, handleUpload, removeButton, useDefaultButton, previewRadius = 'rounded', showEditButtonOnHover = false, disabled, }: VariantProps) { return (
handleUpload()} > {placeholderIcon && !imageUrl && cloneElement(placeholderIcon, {size: 'lg'})}
{children} {(removeButton || useDefaultButton) && (
{removeButton && cloneElement(removeButton, {variant: 'link'})} {useDefaultButton && cloneElement(useDefaultButton, {variant: 'link'})}
)}
); } function AvatarVariant({ children, placeholderIcon, previewSize, isLoading, imageUrl, removeButton, useDefaultButton, handleUpload, previewRadius = 'rounded-full', disabled, }: VariantProps) { if (!placeholderIcon) { placeholderIcon = ( ); } return (
handleUpload()} > {imageUrl ? ( ) : ( placeholderIcon )}
{children} {(removeButton || useDefaultButton) && (
{removeButton && cloneElement(removeButton, {variant: 'link'})} {useDefaultButton && cloneElement(useDefaultButton, {variant: 'link'})}
)}
); } interface FormImageSelectorProps extends ImageSelectorProps { name: string; } export function FormImageSelector(props: FormImageSelectorProps) { const { field: {onChange, value = null}, fieldState: {error}, } = useController({ name: props.name, }); const formProps: Partial = { onChange, value, invalid: error != null, errorMessage: error ? : null, }; return ; }