Uploader.tsx 3.1 KB

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