uploader template
This commit is contained in:
@@ -2,6 +2,7 @@ import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { CloseCircle } from 'iconsax-react'
|
||||
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -9,34 +10,93 @@ type Props = {
|
||||
isMultiple?: boolean,
|
||||
onChange?: (file: File[]) => void;
|
||||
isReset?: boolean;
|
||||
useUploadService?: boolean,
|
||||
onUploadComplete?: (urls: string[]) => void,
|
||||
onUploadError?: (error: Error) => void
|
||||
}
|
||||
|
||||
const UploadBox: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||
|
||||
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||
if (props.useUploadService) {
|
||||
setIsUploading(true)
|
||||
try {
|
||||
const uploadPromises = acceptedFiles.map(file => singleUpload(file))
|
||||
const results = await Promise.all(uploadPromises)
|
||||
const urls = results.map(result => result?.data?.url).filter(Boolean)
|
||||
|
||||
if (props.onUploadComplete) {
|
||||
props.onUploadComplete(urls)
|
||||
}
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
setFiles(array)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
props.onChange && props.onChange(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array)
|
||||
}
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
props.onChange && props.onChange([acceptedFiles[0]])
|
||||
if (props.onChange) {
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
}, [files])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error)
|
||||
if (props.onUploadError) {
|
||||
props.onUploadError(error as Error)
|
||||
}
|
||||
// Fallback to normal file handling
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
setFiles(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array)
|
||||
}
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
if (props.onChange) {
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
} else {
|
||||
// Original behavior
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
setFiles(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array)
|
||||
}
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
if (props.onChange) {
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [files, props.useUploadService, props.onUploadComplete, props.onUploadError, singleUpload])
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
const array = [...files]
|
||||
array.splice(index, 1)
|
||||
setFiles(array)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
props.onChange && props.onChange(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -53,10 +113,10 @@ const UploadBox: FC<Props> = (props: Props) => {
|
||||
<div>
|
||||
<div className='text-sm'>{props.label}</div>
|
||||
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
||||
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
||||
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary' disabled={isUploading}>
|
||||
<input {...getInputProps()} />
|
||||
|
||||
{t('select_file')}
|
||||
{isUploading ? 'در حال آپلود...' : t('select_file')}
|
||||
</button>
|
||||
|
||||
<div className='lg:flex gap-2 hidden'>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { clx } from '../helpers/utils';
|
||||
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
@@ -13,7 +14,10 @@ type Props = {
|
||||
onChangePreview?: (preview: string[]) => void,
|
||||
getCover?: (url: string) => void,
|
||||
coverUrl?: string,
|
||||
isReset?: boolean
|
||||
isReset?: boolean,
|
||||
useUploadService?: boolean,
|
||||
onUploadComplete?: (urls: string[]) => void,
|
||||
onUploadError?: (error: Error) => void
|
||||
}
|
||||
|
||||
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
@@ -23,8 +27,22 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
const [perviews, setPerviews] = useState<string[]>(props.preview ? props.preview : [])
|
||||
const [isFill, setIsFill] = useState<boolean>(false)
|
||||
const [cover, setCover] = useState<string>(props.coverUrl ? props.coverUrl : '')
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||
|
||||
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||
if (props.useUploadService) {
|
||||
setIsUploading(true)
|
||||
try {
|
||||
const uploadPromises = acceptedFiles.map(file => singleUpload(file))
|
||||
const results = await Promise.all(uploadPromises)
|
||||
const urls = results.map(result => result?.data?.url).filter(Boolean)
|
||||
|
||||
if (props.onUploadComplete) {
|
||||
props.onUploadComplete(urls)
|
||||
}
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
@@ -34,7 +52,38 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
setFiles([acceptedFiles[0]])
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
}, [files])
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error)
|
||||
if (props.onUploadError) {
|
||||
props.onUploadError(error as Error)
|
||||
}
|
||||
// Fallback to normal file handling
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
setFiles(array)
|
||||
props.onChange(array)
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
} else {
|
||||
// Original behavior
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
setFiles(array)
|
||||
props.onChange(array)
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
}
|
||||
}, [files, props.useUploadService, props.onUploadComplete, props.onUploadError, singleUpload])
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
@@ -84,7 +133,7 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
color='#8C90A3'
|
||||
/>
|
||||
<div className='text-description text-xs'>
|
||||
{props.label}
|
||||
{isUploading ? 'در حال آپلود...' : props.label}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
|
||||
@@ -2,10 +2,15 @@ import { CloseCircle, Paperclip2 } from 'iconsax-react'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
|
||||
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
useUploadService?: boolean,
|
||||
onUploadComplete?: (urls: string[]) => void,
|
||||
onUploadError?: (error: Error) => void,
|
||||
onChange?: (files: File[]) => void
|
||||
}
|
||||
|
||||
const UploadButton: FC<Props> = (props) => {
|
||||
@@ -14,10 +19,53 @@ const UploadButton: FC<Props> = (props) => {
|
||||
const { title } = props
|
||||
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setFiles((prev) => [...prev, ...acceptedFiles])
|
||||
}, [])
|
||||
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||
if (props.useUploadService) {
|
||||
setIsUploading(true)
|
||||
try {
|
||||
const uploadPromises = acceptedFiles.map(file => singleUpload(file))
|
||||
const results = await Promise.all(uploadPromises)
|
||||
const urls = results.map(result => result?.data?.url).filter(Boolean)
|
||||
|
||||
if (props.onUploadComplete) {
|
||||
props.onUploadComplete(urls)
|
||||
}
|
||||
|
||||
const newFiles = [...files, ...acceptedFiles]
|
||||
setFiles(newFiles)
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(newFiles)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error)
|
||||
if (props.onUploadError) {
|
||||
props.onUploadError(error as Error)
|
||||
}
|
||||
// Fallback to normal file handling
|
||||
const newFiles = [...files, ...acceptedFiles]
|
||||
setFiles(newFiles)
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(newFiles)
|
||||
}
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
} else {
|
||||
// Original behavior
|
||||
const newFiles = [...files, ...acceptedFiles]
|
||||
setFiles(newFiles)
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(newFiles)
|
||||
}
|
||||
}
|
||||
}, [files, props.useUploadService, props.onUploadComplete, props.onUploadError, props.onChange, singleUpload])
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
@@ -25,6 +73,10 @@ const UploadButton: FC<Props> = (props) => {
|
||||
const array = [...files]
|
||||
array.splice(index, 1)
|
||||
setFiles(array)
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(array)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +86,7 @@ const UploadButton: FC<Props> = (props) => {
|
||||
<input {...getInputProps()} />
|
||||
|
||||
<Paperclip2 size={20} color='#000' />
|
||||
<div className='text-xs'>{title ? title : t('attachment_select')}</div>
|
||||
<div className='text-xs'>{isUploading ? 'در حال آپلود...' : (title ? title : t('attachment_select'))}</div>
|
||||
</div>
|
||||
|
||||
{files.map((file, index) => (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FC, useState } from 'react'
|
||||
import { ImageBlock as ImageBlockType } from '../../types/blocks'
|
||||
import Input from '@/components/Input'
|
||||
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
||||
import { useSingleUpload } from '@/pages/setting/personality/hooks/usePersonalityData'
|
||||
|
||||
interface ImageBlockProps {
|
||||
block: ImageBlockType
|
||||
@@ -21,6 +22,9 @@ const ImageBlock: FC<ImageBlockProps> = ({
|
||||
const [localSrc, setLocalSrc] = useState(block.data.src)
|
||||
const [localAlt, setLocalAlt] = useState(block.data.alt)
|
||||
const [localUrl, setLocalUrl] = useState(block.data.url || '')
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
|
||||
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||
|
||||
const handleSrcChange = (src: string) => {
|
||||
setLocalSrc(src)
|
||||
@@ -52,11 +56,26 @@ const ImageBlock: FC<ImageBlockProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleImageUpload = (files: File[]) => {
|
||||
const handleImageUpload = async (files: File[]) => {
|
||||
if (files.length > 0) {
|
||||
const file = files[0]
|
||||
setIsUploading(true)
|
||||
|
||||
try {
|
||||
const uploadResult = await singleUpload(file)
|
||||
const imageUrl = uploadResult?.data?.url
|
||||
|
||||
if (imageUrl) {
|
||||
handleSrcChange(imageUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error)
|
||||
// Fallback to object URL if upload fails
|
||||
const imageUrl = URL.createObjectURL(file)
|
||||
handleSrcChange(imageUrl)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +125,7 @@ const ImageBlock: FC<ImageBlockProps> = ({
|
||||
{isEditing ? (
|
||||
<div className="space-y-3 p-2 bg-gray-50 rounded">
|
||||
<UploadBoxDraggble
|
||||
label="آپلود تصویر"
|
||||
label={isUploading ? "در حال آپلود..." : "آپلود تصویر"}
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
<Input
|
||||
@@ -114,18 +133,21 @@ const ImageBlock: FC<ImageBlockProps> = ({
|
||||
value={localSrc}
|
||||
onChange={(e) => handleSrcChange(e.target.value)}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
disabled={isUploading}
|
||||
/>
|
||||
<Input
|
||||
label="متن جایگزین (Alt Text)"
|
||||
value={localAlt}
|
||||
onChange={(e) => handleAltChange(e.target.value)}
|
||||
placeholder="توضیح تصویر"
|
||||
disabled={isUploading}
|
||||
/>
|
||||
<Input
|
||||
label="لینک تصویر (اختیاری)"
|
||||
value={localUrl}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
placeholder="https://example.com"
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -26,6 +26,7 @@ const Setting: FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_DOMAIN)
|
||||
const [templateName, setTemplateName] = useState('')
|
||||
const [isCapturingScreenshot, setIsCapturingScreenshot] = useState(false)
|
||||
const [isSaveProcessing, setIsSaveProcessing] = useState(false)
|
||||
|
||||
const { mutate: saveTemplate, isPending: isSaving } = useSaveTemplate()
|
||||
const { mutate: updateTemplate, isPending: isUpdating } = useUpdateTemplate()
|
||||
@@ -33,7 +34,7 @@ const Setting: FC = () => {
|
||||
const { mutateAsync: singleUpload, isPending: isUploading } = useSingleUpload()
|
||||
|
||||
const isEditMode = !!id
|
||||
const isLoading = isSaving || isUpdating || isUploading || isCapturingScreenshot
|
||||
const isLoading = isSaving || isUpdating || isUploading || isCapturingScreenshot || isSaveProcessing
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && template?.data?.template?.name) {
|
||||
@@ -102,6 +103,9 @@ const Setting: FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaveProcessing(true)
|
||||
|
||||
try {
|
||||
const html = await exportPersonalityToHTML(data)
|
||||
|
||||
setIsCapturingScreenshot(true)
|
||||
@@ -147,6 +151,9 @@ const Setting: FC = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setIsSaveProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import Button from '@/components/Button'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { AddressData, CreateAddressType, UpdateAddressType } from '../types/Types'
|
||||
import { useGetDomains } from '../../domain/hooks/useDomainData'
|
||||
import { useCreateAddress, useUpdateAddress } from '../hooks/useAddressData'
|
||||
@@ -12,6 +12,7 @@ import { useGetSpace } from '../../domain/hooks/useDomainData'
|
||||
import { useGetTemplates } from '../../personality/hooks/usePersonalityData'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { ErrorType } from '@/helpers/types'
|
||||
import ForwarderManager from './ForwarderManager'
|
||||
|
||||
interface AddressFormProps {
|
||||
selectedAddress: AddressData | null
|
||||
@@ -27,6 +28,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
||||
const { data: spaceData } = useGetSpace()
|
||||
const { mutate: createAddress, isPending } = useCreateAddress()
|
||||
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
|
||||
const [forwarders, setForwarders] = useState<string[]>([])
|
||||
|
||||
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
|
||||
|
||||
@@ -85,6 +87,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
||||
title: params.title,
|
||||
password: params.password || undefined,
|
||||
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||
forwarders: forwarders,
|
||||
}
|
||||
updateAddress({ id: selectedAddress.id, params: updateParams }, {
|
||||
onSuccess: (data) => {
|
||||
@@ -99,11 +102,13 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
||||
const createParams: CreateAddressType = {
|
||||
...params,
|
||||
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||
forwarders: forwarders,
|
||||
}
|
||||
createAddress(createParams, {
|
||||
onSuccess: (data) => {
|
||||
toast(data?.data?.message, 'success')
|
||||
reset()
|
||||
setForwarders([])
|
||||
onSuccess()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
@@ -130,6 +135,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
||||
quotaMB: selectedAddress.emailQuota ? Math.round(selectedAddress.emailQuota / (1024 * 1024)) : undefined,
|
||||
templateId: selectedAddress.template || undefined,
|
||||
})
|
||||
setForwarders(selectedAddress.forwarders || [])
|
||||
} else if (!isEditMode) {
|
||||
reset({
|
||||
title: '',
|
||||
@@ -139,6 +145,7 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
||||
quotaMB: undefined,
|
||||
templateId: domain?.data?.template,
|
||||
})
|
||||
setForwarders([])
|
||||
}
|
||||
}, [selectedAddress, isEditMode, reset, domain])
|
||||
|
||||
@@ -219,6 +226,13 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<ForwarderManager
|
||||
forwarders={forwarders}
|
||||
onChange={setForwarders}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
|
||||
@@ -241,6 +255,8 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<div className='mt-9 flex justify-end'>
|
||||
<Button
|
||||
className='w-fit px-20'
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { FC, useState } from 'react'
|
||||
import Input from '@/components/Input'
|
||||
import Button from '@/components/Button'
|
||||
import { Add, Trash } from 'iconsax-react'
|
||||
import { toast } from '@/components/Toast'
|
||||
|
||||
interface ForwarderManagerProps {
|
||||
forwarders: string[]
|
||||
onChange: (forwarders: string[]) => void
|
||||
error?: string
|
||||
}
|
||||
|
||||
const ForwarderManager: FC<ForwarderManagerProps> = ({ forwarders, onChange, error }) => {
|
||||
const [newForwarder, setNewForwarder] = useState('')
|
||||
|
||||
const validateEmail = (email: string): boolean => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
const addForwarder = () => {
|
||||
if (!newForwarder.trim()) return
|
||||
|
||||
if (!validateEmail(newForwarder)) {
|
||||
toast('لطفاً یک ایمیل معتبر وارد کنید', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (forwarders.includes(newForwarder)) {
|
||||
toast('این ایمیل قبلاً اضافه شده است', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
onChange([...forwarders, newForwarder])
|
||||
setNewForwarder('')
|
||||
}
|
||||
|
||||
const removeForwarder = (index: number) => {
|
||||
const newForwarders = forwarders.filter((_, i) => i !== index)
|
||||
onChange(newForwarders)
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addForwarder()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 mb-2'>
|
||||
Forwarders
|
||||
</label>
|
||||
|
||||
{/* Add new forwarder */}
|
||||
<div className='flex gap-2 mb-3'>
|
||||
<div className='flex-1'>
|
||||
<Input
|
||||
placeholder='example@domain.com'
|
||||
value={newForwarder}
|
||||
onChange={(e) => setNewForwarder(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
className='text-left'
|
||||
dir='ltr'
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type='button'
|
||||
variant='primary'
|
||||
onClick={addForwarder}
|
||||
className='w-fit'
|
||||
disabled={!newForwarder.trim()}
|
||||
>
|
||||
<Add size={16} color='white' />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* List of existing forwarders */}
|
||||
{forwarders.length > 0 && (
|
||||
<div className='space-y-2'>
|
||||
{forwarders.map((forwarder, index) => (
|
||||
<div key={index} className='flex items-center justify-between bg-gray-50 px-2 py-1 rounded-lg'>
|
||||
<span className='text-sm text-right flex-1 ' dir='ltr'>{forwarder}</span>
|
||||
<Button
|
||||
type='button'
|
||||
variant='secondary'
|
||||
onClick={() => removeForwarder(index)}
|
||||
className='w-fit h-9 text-red-500 hover:bg-red-50'
|
||||
>
|
||||
<Trash size={16} color='red' />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{error && (
|
||||
<div className='text-red-500 text-sm mt-1'>{error}</div>
|
||||
)}
|
||||
|
||||
<div className='text-[10px] text-gray-500 mt-2 leading-4'>
|
||||
ایمیلهایی که به این آدرس ارسال میشوند به آدرسهای فوق نیز فرستاده خواهند شد
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForwarderManager
|
||||
@@ -5,6 +5,7 @@ export type CreateAddressType = {
|
||||
title: string;
|
||||
quota?: number;
|
||||
templateId?: string;
|
||||
forwarders?: string[];
|
||||
};
|
||||
|
||||
export type UpdateAddressType = {
|
||||
@@ -13,6 +14,7 @@ export type UpdateAddressType = {
|
||||
quota?: number;
|
||||
aliases?: string[];
|
||||
title?: string;
|
||||
forwarders?: string[];
|
||||
};
|
||||
|
||||
export interface AddressData extends Record<string, unknown> {
|
||||
@@ -32,6 +34,7 @@ export interface AddressData extends Record<string, unknown> {
|
||||
isActive: boolean;
|
||||
emailSignature: string | null;
|
||||
role: string;
|
||||
forwarders?: string[];
|
||||
business: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
|
||||
@@ -57,9 +57,9 @@ const List: FC = () => {
|
||||
)
|
||||
})
|
||||
}
|
||||
<div className='flex-1 rounded-3xl px-4'></div>
|
||||
<div className='flex-1 rounded-3xl px-4'></div>
|
||||
<div className='flex-1 rounded-3xl px-4'></div>
|
||||
<div className='flex-1 max-w-[25%] min-w-[20%] w-full px-4'></div>
|
||||
<div className='flex-1 max-w-[25%] min-w-[20%] w-full px-4'></div>
|
||||
<div className='flex-1 max-w-[25%] min-w-[20%] w-full px-4'></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePersonalityStore } from '../store/Store'
|
||||
import { ImageSize, HorizontalAlignment, VerticalAlignment, ElementType } from '../types/Types'
|
||||
import { useSingleUpload } from '../hooks/usePersonalityData'
|
||||
|
||||
const ImageSideBar: FC = () => {
|
||||
|
||||
@@ -27,6 +28,9 @@ const ImageSideBar: FC = () => {
|
||||
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [isReset, setIsReset] = useState<boolean>(false)
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||
|
||||
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||
|
||||
const isEditMode = selectedElement?.type === ElementType.IMAGE;
|
||||
|
||||
@@ -80,18 +84,31 @@ const ImageSideBar: FC = () => {
|
||||
{ label: 'اندازه اصلی (Auto)', value: ImageSize.AUTO }
|
||||
]
|
||||
|
||||
const handleImageUpload = (files: File[]) => {
|
||||
const handleImageUpload = async (files: File[]) => {
|
||||
if (files.length === 0) return
|
||||
|
||||
const file = files[0] // فقط اولین فایل را در نظر میگیریم
|
||||
// در اینجا باید فایل آپلود شود و URL برگردانده شود
|
||||
// فعلاً از FileReader استفاده میکنم برای تست
|
||||
const file = files[0]
|
||||
setIsUploading(true)
|
||||
|
||||
try {
|
||||
const uploadResult = await singleUpload(file)
|
||||
const imageUrl = uploadResult?.data?.url
|
||||
|
||||
if (imageUrl) {
|
||||
setImageSrc(imageUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error)
|
||||
// Fallback to FileReader if upload fails
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const result = e.target?.result as string
|
||||
setImageSrc(result)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddImage = () => {
|
||||
@@ -198,7 +215,7 @@ const ImageSideBar: FC = () => {
|
||||
|
||||
<div className='mt-8'>
|
||||
<UploadBoxDraggble
|
||||
label={t('setting.upload_image')}
|
||||
label={isUploading ? 'در حال آپلود...' : t('setting.upload_image')}
|
||||
onChange={handleImageUpload}
|
||||
isReset={isReset}
|
||||
/>
|
||||
@@ -218,6 +235,7 @@ const ImageSideBar: FC = () => {
|
||||
value={imageSize}
|
||||
onChange={(e) => setImageSize(e.target.value as ImageSize)}
|
||||
placeholder={t('setting.size')}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -275,8 +293,8 @@ const ImageSideBar: FC = () => {
|
||||
<Button
|
||||
onClick={handleUpdateImage}
|
||||
className='bg-[#0038FF] text-white w-full'
|
||||
disabled={!imageSrc}
|
||||
loading={isLoading}
|
||||
disabled={!imageSrc || isUploading}
|
||||
loading={isLoading || isUploading}
|
||||
>
|
||||
<div className='flex justify-center items-center gap-2'>
|
||||
<Edit size={18} color='white' />
|
||||
@@ -305,8 +323,8 @@ const ImageSideBar: FC = () => {
|
||||
<Button
|
||||
onClick={handleAddImage}
|
||||
className='bg-[#ECEEF5] text-black w-full'
|
||||
disabled={!imageSrc}
|
||||
loading={isLoading}
|
||||
disabled={!imageSrc || isUploading}
|
||||
loading={isLoading || isUploading}
|
||||
>
|
||||
<div className='flex justify-center items-center gap-2'>
|
||||
<Add size={24} color='black' />
|
||||
|
||||
@@ -5,12 +5,17 @@ import ColorPicker from '@/components/ColorPicker';
|
||||
import { usePersonalityStore } from '../store/Store';
|
||||
import { SectionItemType, BackgroundSize, SectionName } from '../types/Types';
|
||||
import Select from '@/components/Select';
|
||||
import { useSingleUpload } from '../hooks/usePersonalityData';
|
||||
import { toast } from '@/components/Toast';
|
||||
|
||||
const SettingSideBar: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { activeSection, setColumnsCount, updateActiveItem, data, activeItemIndex } = usePersonalityStore()
|
||||
const [color, setColor] = useState("#aabbcc");
|
||||
const [backgroundSize, setBackgroundSize] = useState<BackgroundSize>(BackgroundSize.COVER)
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false)
|
||||
|
||||
const { mutateAsync: singleUpload } = useSingleUpload()
|
||||
|
||||
// تشخیص background size بر اساس style موجود
|
||||
const getCurrentBackgroundSizeType = (style?: SectionItemType['style']) => {
|
||||
@@ -49,8 +54,32 @@ const SettingSideBar: FC = () => {
|
||||
updateActiveItem({ backgroundColor: color })
|
||||
}
|
||||
|
||||
const handleUploadBackground = (file: File[]) => {
|
||||
const handleUploadBackground = async (file: File[]) => {
|
||||
if (activeSection && file.length > 0) {
|
||||
setIsUploading(true)
|
||||
|
||||
try {
|
||||
const uploadResult = await singleUpload(file[0])
|
||||
const imageUrl = uploadResult?.data?.url
|
||||
|
||||
if (imageUrl) {
|
||||
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
|
||||
updateActiveItem({
|
||||
backgroundImage: imageUrl,
|
||||
style: {
|
||||
...currentItem?.style,
|
||||
backgroundSize: getBackgroundSize(backgroundSize),
|
||||
backgroundRepeat: getBackgroundRepeat(backgroundSize),
|
||||
backgroundPosition: 'center center'
|
||||
}
|
||||
})
|
||||
toast('تصویر پسزمینه با موفقیت آپلود شد', 'success')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading background image:', error)
|
||||
toast('خطا در آپلود تصویر پسزمینه، از فایل محلی استفاده شد', 'error')
|
||||
|
||||
// Fallback to object URL if upload fails
|
||||
const imageUrl = URL.createObjectURL(file[0])
|
||||
|
||||
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
|
||||
@@ -63,6 +92,9 @@ const SettingSideBar: FC = () => {
|
||||
backgroundPosition: 'center center'
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,35 +142,35 @@ const SettingSideBar: FC = () => {
|
||||
|
||||
<div className='flex h-[51px] gap-5 mt-6'>
|
||||
<div
|
||||
className='w-[102px] h-full bg-[#EAECF4] cursor-pointer hover:bg-[#d1d5db] transition-colors'
|
||||
onClick={() => handleColumnClick(1)}
|
||||
className={`w-[102px] h-full bg-[#EAECF4] transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-[#d1d5db]'}`}
|
||||
onClick={() => !isUploading && handleColumnClick(1)}
|
||||
></div>
|
||||
<div
|
||||
className='w-[102px] h-full flex gap-1 cursor-pointer group'
|
||||
onClick={() => handleColumnClick(2)}
|
||||
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
onClick={() => !isUploading && handleColumnClick(2)}
|
||||
>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex h-[51px] gap-5 mt-5'>
|
||||
<div
|
||||
className='w-[102px] h-full flex gap-1 cursor-pointer group'
|
||||
onClick={() => handleColumnClick(3)}
|
||||
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
onClick={() => !isUploading && handleColumnClick(3)}
|
||||
>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
</div>
|
||||
<div
|
||||
className='w-[102px] h-full flex gap-1 cursor-pointer group'
|
||||
onClick={() => handleColumnClick(4)}
|
||||
className={`w-[102px] h-full flex gap-1 group transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
onClick={() => !isUploading && handleColumnClick(4)}
|
||||
>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
<div className={`flex-1 bg-[#EAECF4] transition-colors ${!isUploading && 'group-hover:bg-[#d1d5db]'}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +184,7 @@ const SettingSideBar: FC = () => {
|
||||
|
||||
<div className='mt-5'>
|
||||
<UploadBoxDraggble
|
||||
label={t('setting.upload_background')}
|
||||
label={isUploading ? 'در حال آپلود...' : t('setting.upload_background')}
|
||||
onChange={handleUploadBackground}
|
||||
/>
|
||||
</div>
|
||||
@@ -177,6 +209,7 @@ const SettingSideBar: FC = () => {
|
||||
})
|
||||
}}
|
||||
placeholder={t('setting.size')}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ const Templete: FC<{
|
||||
|
||||
return (
|
||||
<>
|
||||
<div key={item.id} className='flex-1 bg-white rounded-3xl p-4'>
|
||||
<div key={item.id} className='flex-1 max-w-[25%] min-w-[20%] w-full bg-white rounded-3xl p-4'>
|
||||
<div className='h-[140px] bg-gray-200 rounded-3xl'>
|
||||
<img src={item.thumbnailUrl} alt={item.name} className='w-full h-full object-contain' />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user