You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. 'use client'
  2. import type { ChangeEvent, FC } from 'react'
  3. import { createRef, useEffect, useState } from 'react'
  4. import Cropper, { type Area, type CropperProps } from 'react-easy-crop'
  5. import classNames from 'classnames'
  6. import { useTranslation } from 'react-i18next'
  7. import { ImagePlus } from '../icons/src/vender/line/images'
  8. import { useDraggableUploader } from './hooks'
  9. import { checkIsAnimatedImage } from './utils'
  10. import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
  11. export type OnImageInput = {
  12. (isCropped: true, tempUrl: string, croppedAreaPixels: Area, fileName: string): void
  13. (isCropped: false, file: File): void
  14. }
  15. type UploaderProps = {
  16. className?: string
  17. cropShape?: CropperProps['cropShape']
  18. onImageInput?: OnImageInput
  19. }
  20. const ImageInput: FC<UploaderProps> = ({
  21. className,
  22. cropShape,
  23. onImageInput,
  24. }) => {
  25. const { t } = useTranslation()
  26. const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
  27. const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
  28. useEffect(() => {
  29. return () => {
  30. if (inputImage)
  31. URL.revokeObjectURL(inputImage.url)
  32. }
  33. }, [inputImage])
  34. const [crop, setCrop] = useState({ x: 0, y: 0 })
  35. const [zoom, setZoom] = useState(1)
  36. const onCropComplete = async (_: Area, croppedAreaPixels: Area) => {
  37. if (!inputImage)
  38. return
  39. onImageInput?.(true, inputImage.url, croppedAreaPixels, inputImage.file.name)
  40. }
  41. const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
  42. const file = e.target.files?.[0]
  43. if (file) {
  44. setInputImage({ file, url: URL.createObjectURL(file) })
  45. checkIsAnimatedImage(file).then((isAnimatedImage) => {
  46. setIsAnimatedImage(!!isAnimatedImage)
  47. if (isAnimatedImage)
  48. onImageInput?.(false, file)
  49. })
  50. }
  51. }
  52. const {
  53. isDragActive,
  54. handleDragEnter,
  55. handleDragOver,
  56. handleDragLeave,
  57. handleDrop,
  58. } = useDraggableUploader((file: File) => setInputImage({ file, url: URL.createObjectURL(file) }))
  59. const inputRef = createRef<HTMLInputElement>()
  60. const handleShowImage = () => {
  61. if (isAnimatedImage) {
  62. return (
  63. <img src={inputImage?.url} alt='' />
  64. )
  65. }
  66. return (
  67. <Cropper
  68. image={inputImage?.url}
  69. crop={crop}
  70. zoom={zoom}
  71. aspect={1}
  72. cropShape={cropShape}
  73. onCropChange={setCrop}
  74. onCropComplete={onCropComplete}
  75. onZoomChange={setZoom}
  76. />
  77. )
  78. }
  79. return (
  80. <div className={classNames(className, 'w-full px-3 py-1.5')}>
  81. <div
  82. className={classNames(
  83. isDragActive && 'border-primary-600',
  84. 'relative flex aspect-square flex-col items-center justify-center rounded-lg border-[1.5px] border-dashed text-gray-500')}
  85. onDragEnter={handleDragEnter}
  86. onDragOver={handleDragOver}
  87. onDragLeave={handleDragLeave}
  88. onDrop={handleDrop}
  89. >
  90. {
  91. !inputImage
  92. ? <>
  93. <ImagePlus className="pointer-events-none mb-3 h-[30px] w-[30px]" />
  94. <div className="mb-[2px] text-sm font-medium">
  95. <span className="pointer-events-none">{t('common.imageInput.dropImageHere')}&nbsp;</span>
  96. <button className="text-components-button-primary-bg" onClick={() => inputRef.current?.click()}>{t('common.imageInput.browse')}</button>
  97. <input
  98. ref={inputRef} type="file" className="hidden"
  99. onClick={e => ((e.target as HTMLInputElement).value = '')}
  100. accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')}
  101. onChange={handleLocalFileInput}
  102. />
  103. </div>
  104. <div className="pointer-events-none">{t('common.imageInput.supportedFormats')}</div>
  105. </>
  106. : handleShowImage()
  107. }
  108. </div>
  109. </div>
  110. )
  111. }
  112. export default ImageInput