ImageInput.tsx 3.7 KB

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