Viewing File: /home/markqprx/iniasli.pro/client/ui/images/image-selector.tsx

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<SvgIconProps>;
  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<HTMLInputElement>(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<VariantProps>;
  if (variant === 'avatar') {
    VariantElement = AvatarVariant;
  } else if (variant === 'square') {
    VariantElement = SquareVariant;
  } else {
    VariantElement = InputVariant;
  }

  const removeButton = showRemoveButton ? (
    <Button
      variant="link"
      color="danger"
      size="xs"
      disabled={isDeletingEntry || !imageUrl || disabled}
      onClick={() => {
        deleteEntry({
          onSuccess: () => onChange?.(''),
        });
      }}
    >
      <Trans message="Remove image" />
    </Button>
  ) : null;

  const useDefaultButton =
    defaultValue != null && value !== defaultValue ? (
      <Button
        variant="outline"
        color="primary"
        size="xs"
        disabled={disabled}
        onClick={() => {
          onChange?.(defaultValue);
        }}
      >
        <Trans message="Use default" />
      </Button>
    ) : null;

  const handleUpload = useCallback(() => {
    inputRef.current?.click();
  }, []);

  return (
    <div className={clsx('text-sm', className)}>
      {label && (
        <div id={labelId} className={inputFieldClassNames.label}>
          {label}
        </div>
      )}
      {description && (
        <div className={inputFieldClassNames.description}>{description}</div>
      )}
      <div aria-labelledby={labelId} aria-describedby={descriptionId}>
        <Field
          fieldClassNames={inputFieldClassNames}
          errorMessage={errorMessage}
          invalid={invalid}
        >
          <VariantElement
            inputFieldClassNames={inputFieldClassNames}
            placeholderIcon={placeholderIcon}
            previewSize={previewSize}
            isLoading={uploadStatus === 'inProgress'}
            imageUrl={imageUrl}
            removeButton={removeButton}
            useDefaultButton={useDefaultButton}
            showEditButtonOnHover={showEditButtonOnHover}
            stretchPreview={stretchPreview}
            previewRadius={previewRadius}
            handleUpload={handleUpload}
            disabled={disabled}
          >
            <input
              ref={inputRef}
              aria-labelledby={labelId}
              aria-describedby={descriptionId}
              // if file is already uploaded (from form or via props) set
              // required to false, otherwise farm validation will always fail
              required={imageUrl ? false : required}
              accept={UploadInputType.image}
              type="file"
              disabled={uploadStatus === 'inProgress'}
              className="sr-only"
              onChange={e => {
                if (e.target.files?.length) {
                  uploadFile(e.target.files[0], uploadOptions);
                }
              }}
            />
          </VariantElement>
          {uploadStatus === 'inProgress' && (
            <ProgressBar
              className="absolute left-0 right-0 top-0"
              size="xs"
              value={percentage}
            />
          )}
        </Field>
      </div>
    </div>
  );
}

interface VariantProps {
  children: ReactElement<ComponentPropsWithRef<'input'>>;
  inputFieldClassNames: InputFieldStyle;
  previewSize?: ImageSelectorProps['previewSize'];
  placeholderIcon?: ImageSelectorProps['placeholderIcon'];
  isLoading?: boolean;
  imageUrl?: string;
  removeButton?: ReactElement<ButtonBaseProps> | null;
  useDefaultButton?: ReactElement<ButtonBaseProps> | 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 (
      <Fragment>
        <div
          className={`${previewSize} relative mb-10 overflow-hidden rounded border bg-fg-base/8 p-6`}
        >
          <img
            className={clsx(
              'mx-auto h-full rounded',
              stretchPreview ? 'object-cover' : 'object-contain',
            )}
            onClick={() => handleUpload()}
            src={imageUrl}
            alt=""
          />
          {children}
        </div>
        <Button
          onClick={() => handleUpload()}
          disabled={isLoading || disabled}
          className="mr-10"
          variant="outline"
          color="primary"
          size="xs"
        >
          <Trans message="Replace" />
        </Button>
        {removeButton && cloneElement(removeButton, {variant: 'outline'})}
        {useDefaultButton &&
          cloneElement(useDefaultButton, {variant: 'outline'})}
      </Fragment>
    );
  }
  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 (
    <div>
      <div
        className={clsx(
          previewSize,
          previewRadius,
          !imageUrl && 'border',
          'group z-20 flex flex-col items-center justify-center gap-14 bg-fg-base/8 bg-center bg-no-repeat',
          stretchPreview ? 'bg-cover' : 'bg-contain p-6',
        )}
        style={imageUrl ? {backgroundImage: `url(${imageUrl})`} : undefined}
        onClick={() => handleUpload()}
      >
        {placeholderIcon &&
          !imageUrl &&
          cloneElement(placeholderIcon, {size: 'lg'})}
        <Button
          variant="raised"
          color="white"
          size="xs"
          className={clsx(
            showEditButtonOnHover && 'invisible group-hover:visible',
          )}
          disabled={disabled}
        >
          {imageUrl ? (
            <Trans message="Replace image" />
          ) : (
            <Trans message="Upload image" />
          )}
        </Button>
      </div>
      {children}
      {(removeButton || useDefaultButton) && (
        <div className="mt-8">
          {removeButton && cloneElement(removeButton, {variant: 'link'})}
          {useDefaultButton &&
            cloneElement(useDefaultButton, {variant: 'link'})}
        </div>
      )}
    </div>
  );
}

function AvatarVariant({
  children,
  placeholderIcon,
  previewSize,
  isLoading,
  imageUrl,
  removeButton,
  useDefaultButton,
  handleUpload,
  previewRadius = 'rounded-full',
  disabled,
}: VariantProps) {
  if (!placeholderIcon) {
    placeholderIcon = (
      <AvatarPlaceholderIcon
        viewBox="0 0 48 48"
        className={clsx(
          'h-full w-full bg-primary-light/40 text-primary/40',
          previewRadius,
        )}
      />
    );
  }
  return (
    <div>
      <div
        className={clsx('relative', previewSize)}
        onClick={() => handleUpload()}
      >
        {imageUrl ? (
          <img
            src={imageUrl}
            className={clsx('h-full w-full object-cover', previewRadius)}
            alt=""
          />
        ) : (
          placeholderIcon
        )}
        <div className="absolute -bottom-6 -right-6 rounded-full bg-paper shadow-xl">
          <IconButton
            disabled={isLoading || disabled}
            type="button"
            variant="outline"
            size="sm"
            color="primary"
            radius="rounded-full"
          >
            <AddAPhotoIcon />
          </IconButton>
        </div>
      </div>
      {children}
      {(removeButton || useDefaultButton) && (
        <div className="mt-14">
          {removeButton && cloneElement(removeButton, {variant: 'link'})}
          {useDefaultButton &&
            cloneElement(useDefaultButton, {variant: 'link'})}
        </div>
      )}
    </div>
  );
}

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<ImageSelectorProps> = {
    onChange,
    value,
    invalid: error != null,
    errorMessage: error ? <Trans message="Please select an image." /> : null,
  };

  return <ImageSelector {...mergeProps(formProps, props)} />;
}
Back to Directory File Manager