123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- 'use client'
- import type { Area } from 'react-easy-crop'
- import React, { useCallback, useState } from 'react'
- import { useTranslation } from 'react-i18next'
- import { useContext } from 'use-context-selector'
- import { RiPencilLine } from '@remixicon/react'
- import { updateUserProfile } from '@/service/common'
- import { ToastContext } from '@/app/components/base/toast'
- import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
- import Modal from '@/app/components/base/modal'
- import Divider from '@/app/components/base/divider'
- import Button from '@/app/components/base/button'
- import Avatar, { type AvatarProps } from '@/app/components/base/avatar'
- import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
- import type { ImageFile } from '@/types/app'
- import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
- import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
- type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
- type AvatarWithEditProps = AvatarProps & { onSave?: () => void }
- const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
- const { t } = useTranslation()
- const { notify } = useContext(ToastContext)
- const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
- const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false)
- const [uploading, setUploading] = useState(false)
- const handleImageInput: OnImageInput = useCallback(async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
- setInputImageInfo(
- isCropped
- ? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! }
- : { file: fileOrTempUrl as File },
- )
- }, [setInputImageInfo])
- const handleSaveAvatar = useCallback(async (uploadedFileId: string) => {
- try {
- await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } })
- notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
- setIsShowAvatarPicker(false)
- onSave?.()
- }
- catch (e) {
- notify({ type: 'error', message: (e as Error).message })
- }
- }, [notify, onSave, t])
- const { handleLocalFileUpload } = useLocalFileUploader({
- limit: 3,
- disabled: false,
- onUpload: (imageFile: ImageFile) => {
- if (imageFile.progress === 100) {
- setUploading(false)
- setInputImageInfo(undefined)
- handleSaveAvatar(imageFile.fileId)
- }
- // Error
- if (imageFile.progress === -1)
- setUploading(false)
- },
- })
- const handleSelect = useCallback(async () => {
- if (!inputImageInfo)
- return
- setUploading(true)
- if ('file' in inputImageInfo) {
- handleLocalFileUpload(inputImageInfo.file)
- return
- }
- const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName)
- const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
- handleLocalFileUpload(file)
- }, [handleLocalFileUpload, inputImageInfo])
- if (DISABLE_UPLOAD_IMAGE_AS_ICON)
- return <Avatar {...props} />
- return (
- <>
- <div>
- <div className="relative group">
- <Avatar {...props} />
- <div
- onClick={() => { setIsShowAvatarPicker(true) }}
- className="absolute inset-0 bg-black bg-opacity-50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer flex items-center justify-center"
- >
- <span className="text-white text-xs">
- <RiPencilLine />
- </span>
- </div>
- </div>
- </div>
- <Modal
- closable
- className="!w-[362px] !p-0"
- isShow={isShowAvatarPicker}
- onClose={() => setIsShowAvatarPicker(false)}
- >
- <ImageInput onImageInput={handleImageInput} cropShape='round' />
- <Divider className='m-0' />
- <div className='w-full flex items-center justify-center p-3 gap-2'>
- <Button className='w-full' onClick={() => setIsShowAvatarPicker(false)}>
- {t('app.iconPicker.cancel')}
- </Button>
- <Button variant="primary" className='w-full' disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
- {t('app.iconPicker.ok')}
- </Button>
- </div>
- </Modal>
- </>
- )
- }
- export default AvatarWithEdit
|