Uploader.tsx 3.6 KB

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