ImageInput.tsx 3.8 KB

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