diff --git a/src/components/UploadBox.tsx b/src/components/UploadBox.tsx index 96dad28..702ab69 100644 --- a/src/components/UploadBox.tsx +++ b/src/components/UploadBox.tsx @@ -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) => { const { t } = useTranslation('global') const [files, setFiles] = useState([]) + const [isUploading, setIsUploading] = useState(false) - 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) + 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) + } + + 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]]) + } + } + } 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 { - setFiles([acceptedFiles[0]]) - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - props.onChange && props.onChange([acceptedFiles[0]]) + // 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]) + }, [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.label}
-
diff --git a/src/components/UploadBoxDraggble.tsx b/src/components/UploadBoxDraggble.tsx index cccb109..783be8e 100644 --- a/src/components/UploadBoxDraggble.tsx +++ b/src/components/UploadBoxDraggble.tsx @@ -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) => { @@ -23,18 +27,63 @@ const UploadBoxDraggble: FC = (props: Props) => { const [perviews, setPerviews] = useState(props.preview ? props.preview : []) const [isFill, setIsFill] = useState(false) const [cover, setCover] = useState(props.coverUrl ? props.coverUrl : '') + const [isUploading, setIsUploading] = useState(false) - const onDrop = useCallback((acceptedFiles: File[]) => { - if (props.isMultiple) { - const array = [...files] - array.push(acceptedFiles[0]) - setFiles(array) - props.onChange(array) + 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) + } + + if (props.isMultiple) { + const array = [...files] + array.push(acceptedFiles[0]) + setFiles(array) + props.onChange(array) + } else { + setFiles([acceptedFiles[0]]) + props.onChange([acceptedFiles[0]]) + } + } 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 { - setFiles([acceptedFiles[0]]) - props.onChange([acceptedFiles[0]]) + // 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]) + }, [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) => { color='#8C90A3' />
- {props.label} + {isUploading ? 'در حال آپلود...' : props.label}
diff --git a/src/components/UploadButton.tsx b/src/components/UploadButton.tsx index a2d597c..13b79ee 100644 --- a/src/components/UploadButton.tsx +++ b/src/components/UploadButton.tsx @@ -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) => { @@ -14,10 +19,53 @@ const UploadButton: FC = (props) => { const { title } = props const [files, setFiles] = useState([]) + const [isUploading, setIsUploading] = useState(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) => { const array = [...files] array.splice(index, 1) setFiles(array) + + if (props.onChange) { + props.onChange(array) + } } @@ -34,7 +86,7 @@ const UploadButton: FC = (props) => { -
{title ? title : t('attachment_select')}
+
{isUploading ? 'در حال آپلود...' : (title ? title : t('attachment_select'))}
{files.map((file, index) => ( diff --git a/src/pages/email-builder/components/blocks/ImageBlock.tsx b/src/pages/email-builder/components/blocks/ImageBlock.tsx index 0410ef4..cc40112 100644 --- a/src/pages/email-builder/components/blocks/ImageBlock.tsx +++ b/src/pages/email-builder/components/blocks/ImageBlock.tsx @@ -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 = ({ 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 = ({ }) } - const handleImageUpload = (files: File[]) => { + const handleImageUpload = async (files: File[]) => { if (files.length > 0) { const file = files[0] - const imageUrl = URL.createObjectURL(file) - handleSrcChange(imageUrl) + 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 = ({ {isEditing ? (
= ({ value={localSrc} onChange={(e) => handleSrcChange(e.target.value)} placeholder="https://example.com/image.jpg" + disabled={isUploading} /> handleAltChange(e.target.value)} placeholder="توضیح تصویر" + disabled={isUploading} /> handleUrlChange(e.target.value)} placeholder="https://example.com" + disabled={isUploading} />
) : ( diff --git a/src/pages/setting/Setting.tsx b/src/pages/setting/Setting.tsx index 993a2fb..b629af6 100644 --- a/src/pages/setting/Setting.tsx +++ b/src/pages/setting/Setting.tsx @@ -26,6 +26,7 @@ const Setting: FC = () => { const [activeTab, setActiveTab] = useState(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,50 +103,56 @@ const Setting: FC = () => { return } - const html = await exportPersonalityToHTML(data) + setIsSaveProcessing(true) - setIsCapturingScreenshot(true) - const screenshotFile = await capturePersonalityScreenshot(templateName) - setIsCapturingScreenshot(false) + try { + const html = await exportPersonalityToHTML(data) - if (!screenshotFile) { - toast('خطا در گرفتن اسکرین‌شات', 'error') - return - } + setIsCapturingScreenshot(true) + const screenshotFile = await capturePersonalityScreenshot(templateName) + setIsCapturingScreenshot(false) - const thumbnailUrl = await singleUpload(screenshotFile) + if (!screenshotFile) { + toast('خطا در گرفتن اسکرین‌شات', 'error') + return + } - if (isEditMode) { - updateTemplate({ - id: id!, - params: { + const thumbnailUrl = await singleUpload(screenshotFile) + + if (isEditMode) { + updateTemplate({ + id: id!, + params: { + name: templateName, + rawHtml: html, + structure: data, + thumbnailUrl: thumbnailUrl?.data?.url + } + }, { + onSuccess: (data) => { + toast(data?.data?.message || 'قالب با موفقیت بروزرسانی شد', 'success') + }, + onError: () => { + toast('خطا در بروزرسانی قالب', 'error') + } + }) + } else { + saveTemplate({ name: templateName, rawHtml: html, structure: data, thumbnailUrl: thumbnailUrl?.data?.url - } - }, { - onSuccess: (data) => { - toast(data?.data?.message || 'قالب با موفقیت بروزرسانی شد', 'success') - }, - onError: () => { - toast('خطا در بروزرسانی قالب', 'error') - } - }) - } else { - saveTemplate({ - name: templateName, - rawHtml: html, - structure: data, - thumbnailUrl: thumbnailUrl?.data?.url - }, { - onSuccess: (data) => { - toast(data?.data?.message || 'قالب با موفقیت ایجاد شد', 'success') - }, - onError: () => { - toast('خطا در ایجاد قالب', 'error') - } - }) + }, { + onSuccess: (data) => { + toast(data?.data?.message || 'قالب با موفقیت ایجاد شد', 'success') + }, + onError: () => { + toast('خطا در ایجاد قالب', 'error') + } + }) + } + } finally { + setIsSaveProcessing(false) } } diff --git a/src/pages/setting/address/components/AddressForm.tsx b/src/pages/setting/address/components/AddressForm.tsx index d13f474..62f5817 100644 --- a/src/pages/setting/address/components/AddressForm.tsx +++ b/src/pages/setting/address/components/AddressForm.tsx @@ -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 = ({ selectedAddress, isEditMode, onCanc const { data: spaceData } = useGetSpace() const { mutate: createAddress, isPending } = useCreateAddress() const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress() + const [forwarders, setForwarders] = useState([]) type FormData = CreateAddressType & { password?: string; quotaMB?: number } @@ -85,6 +87,7 @@ const AddressForm: FC = ({ 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 = ({ 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 = ({ 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 = ({ selectedAddress, isEditMode, onCanc quotaMB: undefined, templateId: domain?.data?.template, }) + setForwarders([]) } }, [selectedAddress, isEditMode, reset, domain]) @@ -219,6 +226,13 @@ const AddressForm: FC = ({ selectedAddress, isEditMode, onCanc
+
+ +
+
= ({ selectedAddress, isEditMode, onCanc
)} + +
+
+ + {/* List of existing forwarders */} + {forwarders.length > 0 && ( +
+ {forwarders.map((forwarder, index) => ( +
+ {forwarder} + +
+ ))} +
+ )} + + + + {error && ( +
{error}
+ )} + +
+ ایمیل‌هایی که به این آدرس ارسال می‌شوند به آدرس‌های فوق نیز فرستاده خواهند شد +
+
+ ) +} + +export default ForwarderManager \ No newline at end of file diff --git a/src/pages/setting/address/types/Types.ts b/src/pages/setting/address/types/Types.ts index 28294cb..8f287a9 100644 --- a/src/pages/setting/address/types/Types.ts +++ b/src/pages/setting/address/types/Types.ts @@ -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 { @@ -32,6 +34,7 @@ export interface AddressData extends Record { isActive: boolean; emailSignature: string | null; role: string; + forwarders?: string[]; business: { id: string; createdAt: string; diff --git a/src/pages/setting/personality/List.tsx b/src/pages/setting/personality/List.tsx index a2cf118..68309ba 100644 --- a/src/pages/setting/personality/List.tsx +++ b/src/pages/setting/personality/List.tsx @@ -57,9 +57,9 @@ const List: FC = () => { ) }) } -
-
-
+
+
+
diff --git a/src/pages/setting/personality/components/ImageSideBar.tsx b/src/pages/setting/personality/components/ImageSideBar.tsx index 4ffe223..3786425 100644 --- a/src/pages/setting/personality/components/ImageSideBar.tsx +++ b/src/pages/setting/personality/components/ImageSideBar.tsx @@ -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.MIDDLE) const [isLoading, setIsLoading] = useState(false) const [isReset, setIsReset] = useState(false) + const [isUploading, setIsUploading] = useState(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 reader = new FileReader() - reader.onload = (e) => { - const result = e.target?.result as string - setImageSrc(result) + 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) } - reader.readAsDataURL(file) } const handleAddImage = () => { @@ -198,7 +215,7 @@ const ImageSideBar: FC = () => {
@@ -218,6 +235,7 @@ const ImageSideBar: FC = () => { value={imageSize} onChange={(e) => setImageSize(e.target.value as ImageSize)} placeholder={t('setting.size')} + disabled={isUploading} />
@@ -275,8 +293,8 @@ const ImageSideBar: FC = () => {