| @@ -24,7 +24,7 @@ const WarningMask: FC<IWarningMaskProps> = ({ | |||
| return ( | |||
| <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='mt-4 text-[24px] leading-normal font-semibold text-gray-800'> | |||
| {title} | |||
| @@ -1,6 +1,7 @@ | |||
| import type { FC } from 'react' | |||
| import { useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import cn from 'classnames' | |||
| import Uploader from './uploader' | |||
| import ImageLinkInput from './image-link-input' | |||
| import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images' | |||
| @@ -25,16 +26,16 @@ const UploadOnlyFromLocal: FC<UploadOnlyFromLocalProps> = ({ | |||
| }) => { | |||
| return ( | |||
| <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 | |||
| ${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> | |||
| ) | |||
| } | |||
| @@ -54,13 +55,16 @@ const UploaderButton: FC<UploaderButtonProps> = ({ | |||
| const { t } = useTranslation() | |||
| 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) => { | |||
| setOpen(false) | |||
| onUpload(imageFile) | |||
| } | |||
| const closePopover = () => setOpen(false) | |||
| const handleToggle = () => { | |||
| if (disabled) | |||
| return | |||
| @@ -72,43 +76,46 @@ const UploaderButton: FC<UploaderButtonProps> = ({ | |||
| <PortalToFollowElem | |||
| open={open} | |||
| onOpenChange={setOpen} | |||
| placement='top-start' | |||
| placement="top-start" | |||
| > | |||
| <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> | |||
| <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} /> | |||
| { | |||
| 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> | |||
| </PortalToFollowElemContent> | |||
| </PortalToFollowElem> | |||
| @@ -125,7 +132,9 @@ const ChatImageUploader: FC<ChatImageUploaderProps> = ({ | |||
| onUpload, | |||
| 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) { | |||
| return ( | |||
| @@ -30,6 +30,7 @@ const ImageLinkInput: FC<ImageLinkInputProps> = ({ | |||
| return ( | |||
| <div className='flex items-center pl-1.5 pr-1 h-8 border border-gray-200 bg-white shadow-xs rounded-lg'> | |||
| <input | |||
| type="text" | |||
| className='grow mr-0.5 px-1 h-[18px] text-[13px] outline-none appearance-none' | |||
| value={imageLink} | |||
| onChange={e => setImageLink(e.target.value)} | |||
| @@ -1,7 +1,11 @@ | |||
| import type { FC } from 'react' | |||
| import { useState } from 'react' | |||
| 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 { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' | |||
| import TooltipPlus from '@/app/components/base/tooltip-plus' | |||
| @@ -30,7 +34,11 @@ const ImageList: FC<ImageListProps> = ({ | |||
| const [imagePreviewUrl, setImagePreviewUrl] = useState('') | |||
| 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) | |||
| } | |||
| const handleImageLinkLoadError = (item: ImageFile) => { | |||
| @@ -39,89 +47,95 @@ const ImageList: FC<ImageListProps> = ({ | |||
| } | |||
| 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 | |||
| ${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> | |||
| ) | |||
| } | |||
| @@ -7,6 +7,7 @@ import { ALLOW_FILE_EXTENSIONS } from '@/types/app' | |||
| type UploaderProps = { | |||
| children: (hovering: boolean) => JSX.Element | |||
| onUpload: (imageFile: ImageFile) => void | |||
| closePopover?: () => void | |||
| limit?: number | |||
| disabled?: boolean | |||
| } | |||
| @@ -14,11 +15,16 @@ type UploaderProps = { | |||
| const Uploader: FC<UploaderProps> = ({ | |||
| children, | |||
| onUpload, | |||
| closePopover, | |||
| limit, | |||
| disabled, | |||
| }) => { | |||
| const [hovering, setHovering] = useState(false) | |||
| const { handleLocalFileUpload } = useLocalFileUploader({ limit, onUpload, disabled }) | |||
| const { handleLocalFileUpload } = useLocalFileUploader({ | |||
| limit, | |||
| onUpload, | |||
| disabled, | |||
| }) | |||
| const handleChange = (e: ChangeEvent<HTMLInputElement>) => { | |||
| const file = e.target.files?.[0] | |||
| @@ -27,6 +33,7 @@ const Uploader: FC<UploaderProps> = ({ | |||
| return | |||
| handleLocalFileUpload(file) | |||
| closePopover?.() | |||
| } | |||
| return ( | |||
| @@ -37,11 +44,8 @@ const Uploader: FC<UploaderProps> = ({ | |||
| > | |||
| {children(hovering)} | |||
| <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' | |||
| accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} | |||
| onChange={handleChange} | |||