| return ( | return ( | ||||
| <div className={`${s.mask} absolute z-10 inset-0 pt-16`} | <div className={`${s.mask} absolute z-10 inset-0 pt-16`} | ||||
| > | > | ||||
| <div className='mx-auto w-[535px]'> | |||||
| <div className='mx-auto px-10'> | |||||
| <div className={`${s.icon} flex items-center justify-center w-11 h-11 rounded-xl bg-white`}>{warningIcon}</div> | <div className={`${s.icon} flex items-center justify-center w-11 h-11 rounded-xl bg-white`}>{warningIcon}</div> | ||||
| <div className='mt-4 text-[24px] leading-normal font-semibold text-gray-800'> | <div className='mt-4 text-[24px] leading-normal font-semibold text-gray-800'> | ||||
| {title} | {title} |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { useState } from 'react' | import { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import cn from 'classnames' | |||||
| import Uploader from './uploader' | import Uploader from './uploader' | ||||
| import ImageLinkInput from './image-link-input' | import ImageLinkInput from './image-link-input' | ||||
| import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images' | import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images' | ||||
| }) => { | }) => { | ||||
| return ( | return ( | ||||
| <Uploader onUpload={onUpload} disabled={disabled} limit={limit}> | <Uploader onUpload={onUpload} disabled={disabled} limit={limit}> | ||||
| { | |||||
| hovering => ( | |||||
| <div className={` | |||||
| {hovering => ( | |||||
| <div | |||||
| className={` | |||||
| relative flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer | relative flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer | ||||
| ${hovering && 'bg-gray-100'} | ${hovering && 'bg-gray-100'} | ||||
| `}> | |||||
| <ImagePlus className='w-4 h-4 text-gray-500' /> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| `} | |||||
| > | |||||
| <ImagePlus className="w-4 h-4 text-gray-500" /> | |||||
| </div> | |||||
| )} | |||||
| </Uploader> | </Uploader> | ||||
| ) | ) | ||||
| } | } | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const [open, setOpen] = useState(false) | const [open, setOpen] = useState(false) | ||||
| const hasUploadFromLocal = methods.find(method => method === TransferMethod.local_file) | |||||
| const hasUploadFromLocal = methods.find( | |||||
| method => method === TransferMethod.local_file, | |||||
| ) | |||||
| const handleUpload = (imageFile: ImageFile) => { | const handleUpload = (imageFile: ImageFile) => { | ||||
| setOpen(false) | |||||
| onUpload(imageFile) | onUpload(imageFile) | ||||
| } | } | ||||
| const closePopover = () => setOpen(false) | |||||
| const handleToggle = () => { | const handleToggle = () => { | ||||
| if (disabled) | if (disabled) | ||||
| return | return | ||||
| <PortalToFollowElem | <PortalToFollowElem | ||||
| open={open} | open={open} | ||||
| onOpenChange={setOpen} | onOpenChange={setOpen} | ||||
| placement='top-start' | |||||
| placement="top-start" | |||||
| > | > | ||||
| <PortalToFollowElemTrigger onClick={handleToggle}> | <PortalToFollowElemTrigger onClick={handleToggle}> | ||||
| <div className={` | |||||
| relative flex items-center justify-center w-8 h-8 hover:bg-gray-100 rounded-lg | |||||
| ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'} | |||||
| `}> | |||||
| <ImagePlus className='w-4 h-4 text-gray-500' /> | |||||
| </div> | |||||
| <button | |||||
| type="button" | |||||
| disabled={disabled} | |||||
| className="relative flex items-center justify-center w-8 h-8 enabled:hover:bg-gray-100 rounded-lg disabled:cursor-not-allowed" | |||||
| > | |||||
| <ImagePlus className="w-4 h-4 text-gray-500" /> | |||||
| </button> | |||||
| </PortalToFollowElemTrigger> | </PortalToFollowElemTrigger> | ||||
| <PortalToFollowElemContent className='z-50'> | |||||
| <div className='p-2 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'> | |||||
| <PortalToFollowElemContent className="z-50"> | |||||
| <div className="p-2 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg"> | |||||
| <ImageLinkInput onUpload={handleUpload} /> | <ImageLinkInput onUpload={handleUpload} /> | ||||
| { | |||||
| hasUploadFromLocal && ( | |||||
| <> | |||||
| <div className='flex items-center mt-2 px-2 text-xs font-medium text-gray-400'> | |||||
| <div className='mr-3 w-[93px] h-[1px] bg-gradient-to-l from-[#F3F4F6]' /> | |||||
| OR | |||||
| <div className='ml-3 w-[93px] h-[1px] bg-gradient-to-r from-[#F3F4F6]' /> | |||||
| </div> | |||||
| <Uploader onUpload={handleUpload} limit={limit}> | |||||
| { | |||||
| hovering => ( | |||||
| <div className={` | |||||
| flex items-center justify-center h-8 text-[13px] font-medium text-[#155EEF] rounded-lg cursor-pointer | |||||
| ${hovering && 'bg-primary-50'} | |||||
| `}> | |||||
| <Upload03 className='mr-1 w-4 h-4' /> | |||||
| {t('common.imageUploader.uploadFromComputer')} | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </Uploader> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| {hasUploadFromLocal && ( | |||||
| <> | |||||
| <div className="flex items-center mt-2 px-2 text-xs font-medium text-gray-400"> | |||||
| <div className="mr-3 w-[93px] h-[1px] bg-gradient-to-l from-[#F3F4F6]" /> | |||||
| OR | |||||
| <div className="ml-3 w-[93px] h-[1px] bg-gradient-to-r from-[#F3F4F6]" /> | |||||
| </div> | |||||
| <Uploader | |||||
| onUpload={handleUpload} | |||||
| limit={limit} | |||||
| closePopover={closePopover} | |||||
| > | |||||
| {hovering => ( | |||||
| <div | |||||
| className={cn( | |||||
| 'flex items-center justify-center h-8 text-[13px] font-medium text-[#155EEF] rounded-lg cursor-pointer', | |||||
| hovering && 'bg-primary-50', | |||||
| )} | |||||
| > | |||||
| <Upload03 className="mr-1 w-4 h-4" /> | |||||
| {t('common.imageUploader.uploadFromComputer')} | |||||
| </div> | |||||
| )} | |||||
| </Uploader> | |||||
| </> | |||||
| )} | |||||
| </div> | </div> | ||||
| </PortalToFollowElemContent> | </PortalToFollowElemContent> | ||||
| </PortalToFollowElem> | </PortalToFollowElem> | ||||
| onUpload, | onUpload, | ||||
| disabled, | disabled, | ||||
| }) => { | }) => { | ||||
| const onlyUploadLocal = settings.transfer_methods.length === 1 && settings.transfer_methods[0] === TransferMethod.local_file | |||||
| const onlyUploadLocal | |||||
| = settings.transfer_methods.length === 1 | |||||
| && settings.transfer_methods[0] === TransferMethod.local_file | |||||
| if (onlyUploadLocal) { | if (onlyUploadLocal) { | ||||
| return ( | return ( |
| return ( | return ( | ||||
| <div className='flex items-center pl-1.5 pr-1 h-8 border border-gray-200 bg-white shadow-xs rounded-lg'> | <div className='flex items-center pl-1.5 pr-1 h-8 border border-gray-200 bg-white shadow-xs rounded-lg'> | ||||
| <input | <input | ||||
| type="text" | |||||
| className='grow mr-0.5 px-1 h-[18px] text-[13px] outline-none appearance-none' | className='grow mr-0.5 px-1 h-[18px] text-[13px] outline-none appearance-none' | ||||
| value={imageLink} | value={imageLink} | ||||
| onChange={e => setImageLink(e.target.value)} | onChange={e => setImageLink(e.target.value)} |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { useState } from 'react' | import { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { Loading02, XClose } from '@/app/components/base/icons/src/vender/line/general' | |||||
| import cn from 'classnames' | |||||
| import { | |||||
| Loading02, | |||||
| XClose, | |||||
| } from '@/app/components/base/icons/src/vender/line/general' | |||||
| import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' | import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' | ||||
| import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' | import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' | ||||
| import TooltipPlus from '@/app/components/base/tooltip-plus' | import TooltipPlus from '@/app/components/base/tooltip-plus' | ||||
| const [imagePreviewUrl, setImagePreviewUrl] = useState('') | const [imagePreviewUrl, setImagePreviewUrl] = useState('') | ||||
| const handleImageLinkLoadSuccess = (item: ImageFile) => { | const handleImageLinkLoadSuccess = (item: ImageFile) => { | ||||
| if (item.type === TransferMethod.remote_url && onImageLinkLoadSuccess && item.progress !== -1) | |||||
| if ( | |||||
| item.type === TransferMethod.remote_url | |||||
| && onImageLinkLoadSuccess | |||||
| && item.progress !== -1 | |||||
| ) | |||||
| onImageLinkLoadSuccess(item._id) | onImageLinkLoadSuccess(item._id) | ||||
| } | } | ||||
| const handleImageLinkLoadError = (item: ImageFile) => { | const handleImageLinkLoadError = (item: ImageFile) => { | ||||
| } | } | ||||
| return ( | return ( | ||||
| <div className='flex flex-wrap'> | |||||
| { | |||||
| list.map(item => ( | |||||
| <div | |||||
| key={item._id} | |||||
| className='group relative mr-1 border-[0.5px] border-black/5 rounded-lg' | |||||
| > | |||||
| { | |||||
| item.type === TransferMethod.local_file && item.progress !== 100 && ( | |||||
| <> | |||||
| <div | |||||
| className='absolute inset-0 flex items-center justify-center z-[1] bg-black/30' | |||||
| style={{ left: item.progress > -1 ? `${item.progress}%` : 0 }} | |||||
| > | |||||
| { | |||||
| item.progress === -1 && ( | |||||
| <RefreshCcw01 className='w-5 h-5 text-white' onClick={() => onReUpload && onReUpload(item._id)} /> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| { | |||||
| item.progress > -1 && ( | |||||
| <span className='absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] text-sm text-white mix-blend-lighten z-[1]'>{item.progress}%</span> | |||||
| ) | |||||
| } | |||||
| </> | |||||
| ) | |||||
| } | |||||
| { | |||||
| item.type === TransferMethod.remote_url && item.progress !== 100 && ( | |||||
| <div className={` | |||||
| <div className="flex flex-wrap"> | |||||
| {list.map(item => ( | |||||
| <div | |||||
| key={item._id} | |||||
| className="group relative mr-1 border-[0.5px] border-black/5 rounded-lg" | |||||
| > | |||||
| {item.type === TransferMethod.local_file && item.progress !== 100 && ( | |||||
| <> | |||||
| <div | |||||
| className="absolute inset-0 flex items-center justify-center z-[1] bg-black/30" | |||||
| style={{ left: item.progress > -1 ? `${item.progress}%` : 0 }} | |||||
| > | |||||
| {item.progress === -1 && ( | |||||
| <RefreshCcw01 | |||||
| className="w-5 h-5 text-white" | |||||
| onClick={() => onReUpload && onReUpload(item._id)} | |||||
| /> | |||||
| )} | |||||
| </div> | |||||
| {item.progress > -1 && ( | |||||
| <span className="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] text-sm text-white mix-blend-lighten z-[1]"> | |||||
| {item.progress}% | |||||
| </span> | |||||
| )} | |||||
| </> | |||||
| )} | |||||
| {item.type === TransferMethod.remote_url && item.progress !== 100 && ( | |||||
| <div | |||||
| className={` | |||||
| absolute inset-0 flex items-center justify-center rounded-lg z-[1] border | absolute inset-0 flex items-center justify-center rounded-lg z-[1] border | ||||
| ${item.progress === -1 ? 'bg-[#FEF0C7] border-[#DC6803]' : 'bg-black/[0.16] border-transparent'} | |||||
| `}> | |||||
| { | |||||
| item.progress > -1 && ( | |||||
| <Loading02 className='animate-spin w-5 h-5 text-white' /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| item.progress === -1 && ( | |||||
| <TooltipPlus popupContent={t('common.imageUploader.pasteImageLinkInvalid')}> | |||||
| <AlertTriangle className='w-4 h-4 text-[#DC6803]' /> | |||||
| </TooltipPlus> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| ) | |||||
| ${ | |||||
| item.progress === -1 | |||||
| ? 'bg-[#FEF0C7] border-[#DC6803]' | |||||
| : 'bg-black/[0.16] border-transparent' | |||||
| } | } | ||||
| <img | |||||
| className='w-16 h-16 rounded-lg object-cover cursor-pointer border-[0.5px] border-black/5' | |||||
| alt='' | |||||
| onLoad={() => handleImageLinkLoadSuccess(item)} | |||||
| onError={() => handleImageLinkLoadError(item)} | |||||
| src={item.type === TransferMethod.remote_url ? item.url : item.base64Url} | |||||
| onClick={() => item.progress === 100 && setImagePreviewUrl((item.type === TransferMethod.remote_url ? item.url : item.base64Url) as string)} | |||||
| /> | |||||
| { | |||||
| !readonly && ( | |||||
| <div | |||||
| className={` | |||||
| absolute z-10 -top-[9px] -right-[9px] items-center justify-center w-[18px] h-[18px] | |||||
| bg-white hover:bg-gray-50 border-[0.5px] border-black/[0.02] rounded-2xl shadow-lg | |||||
| cursor-pointer | |||||
| ${item.progress === -1 ? 'flex' : 'hidden group-hover:flex'} | |||||
| `} | |||||
| onClick={() => onRemove && onRemove(item._id)} | |||||
| `} | |||||
| > | |||||
| {item.progress > -1 && ( | |||||
| <Loading02 className="animate-spin w-5 h-5 text-white" /> | |||||
| )} | |||||
| {item.progress === -1 && ( | |||||
| <TooltipPlus | |||||
| popupContent={t('common.imageUploader.pasteImageLinkInvalid')} | |||||
| > | > | ||||
| <XClose className='w-3 h-3 text-gray-500' /> | |||||
| </div> | |||||
| <AlertTriangle className="w-4 h-4 text-[#DC6803]" /> | |||||
| </TooltipPlus> | |||||
| )} | |||||
| </div> | |||||
| )} | |||||
| <img | |||||
| className="w-16 h-16 rounded-lg object-cover cursor-pointer border-[0.5px] border-black/5" | |||||
| alt={item.file?.name} | |||||
| onLoad={() => handleImageLinkLoadSuccess(item)} | |||||
| onError={() => handleImageLinkLoadError(item)} | |||||
| src={ | |||||
| item.type === TransferMethod.remote_url | |||||
| ? item.url | |||||
| : item.base64Url | |||||
| } | |||||
| onClick={() => | |||||
| item.progress === 100 | |||||
| && setImagePreviewUrl( | |||||
| (item.type === TransferMethod.remote_url | |||||
| ? item.url | |||||
| : item.base64Url) as string, | |||||
| ) | ) | ||||
| } | } | ||||
| </div> | |||||
| )) | |||||
| } | |||||
| { | |||||
| imagePreviewUrl && ( | |||||
| <ImagePreview | |||||
| url={imagePreviewUrl} | |||||
| onCancel={() => setImagePreviewUrl('')} | |||||
| /> | /> | ||||
| ) | |||||
| } | |||||
| {!readonly && ( | |||||
| <button | |||||
| type="button" | |||||
| className={cn( | |||||
| 'absolute z-10 -top-[9px] -right-[9px] items-center justify-center w-[18px] h-[18px]', | |||||
| 'bg-white hover:bg-gray-50 border-[0.5px] border-black/[0.02] rounded-2xl shadow-lg', | |||||
| item.progress === -1 ? 'flex' : 'hidden group-hover:flex', | |||||
| )} | |||||
| onClick={() => onRemove && onRemove(item._id)} | |||||
| > | |||||
| <XClose className="w-3 h-3 text-gray-500" /> | |||||
| </button> | |||||
| )} | |||||
| </div> | |||||
| ))} | |||||
| {imagePreviewUrl && ( | |||||
| <ImagePreview | |||||
| url={imagePreviewUrl} | |||||
| onCancel={() => setImagePreviewUrl('')} | |||||
| /> | |||||
| )} | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } |
| type UploaderProps = { | type UploaderProps = { | ||||
| children: (hovering: boolean) => JSX.Element | children: (hovering: boolean) => JSX.Element | ||||
| onUpload: (imageFile: ImageFile) => void | onUpload: (imageFile: ImageFile) => void | ||||
| closePopover?: () => void | |||||
| limit?: number | limit?: number | ||||
| disabled?: boolean | disabled?: boolean | ||||
| } | } | ||||
| const Uploader: FC<UploaderProps> = ({ | const Uploader: FC<UploaderProps> = ({ | ||||
| children, | children, | ||||
| onUpload, | onUpload, | ||||
| closePopover, | |||||
| limit, | limit, | ||||
| disabled, | disabled, | ||||
| }) => { | }) => { | ||||
| const [hovering, setHovering] = useState(false) | const [hovering, setHovering] = useState(false) | ||||
| const { handleLocalFileUpload } = useLocalFileUploader({ limit, onUpload, disabled }) | |||||
| const { handleLocalFileUpload } = useLocalFileUploader({ | |||||
| limit, | |||||
| onUpload, | |||||
| disabled, | |||||
| }) | |||||
| const handleChange = (e: ChangeEvent<HTMLInputElement>) => { | const handleChange = (e: ChangeEvent<HTMLInputElement>) => { | ||||
| const file = e.target.files?.[0] | const file = e.target.files?.[0] | ||||
| return | return | ||||
| handleLocalFileUpload(file) | handleLocalFileUpload(file) | ||||
| closePopover?.() | |||||
| } | } | ||||
| return ( | return ( | ||||
| > | > | ||||
| {children(hovering)} | {children(hovering)} | ||||
| <input | <input | ||||
| className={` | |||||
| absolute block inset-0 opacity-0 text-[0] w-full | |||||
| ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'} | |||||
| `} | |||||
| onClick={e => (e.target as HTMLInputElement).value = ''} | |||||
| className='absolute block inset-0 opacity-0 text-[0] w-full disabled:cursor-not-allowed cursor-pointer' | |||||
| onClick={e => ((e.target as HTMLInputElement).value = '')} | |||||
| type='file' | type='file' | ||||
| accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} | accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} | ||||
| onChange={handleChange} | onChange={handleChange} |