diff --git a/src/components/UploadButton.tsx b/src/components/UploadButton.tsx index a2d597c..13cca02 100644 --- a/src/components/UploadButton.tsx +++ b/src/components/UploadButton.tsx @@ -6,28 +6,34 @@ import { useDropzone } from 'react-dropzone' type Props = { title?: string + onChange?: (files: File[]) => void + onRemove?: (files: File[]) => void } const UploadButton: FC = (props) => { const { t } = useTranslation() - const { title } = props + const { title, onChange, onRemove } = props const [files, setFiles] = useState([]) const onDrop = useCallback((acceptedFiles: File[]) => { - setFiles((prev) => [...prev, ...acceptedFiles]) - }, []) + const updatedFiles = [...files, ...acceptedFiles] + setFiles(updatedFiles) + onChange?.(updatedFiles) + }, [files, onChange]) const { getRootProps, getInputProps } = useDropzone({ onDrop }) const handleRemove = (index: number) => { - const array = [...files] - array.splice(index, 1) - setFiles(array) + const updatedFiles = files.filter((_, i) => i !== index) + setFiles(updatedFiles) + onRemove?.(updatedFiles) } + + return (
diff --git a/src/components/newMessage/EmailAttachments.tsx b/src/components/newMessage/EmailAttachments.tsx new file mode 100644 index 0000000..cba8f57 --- /dev/null +++ b/src/components/newMessage/EmailAttachments.tsx @@ -0,0 +1,69 @@ +import { FC } from 'react' +import UploadButton from '@/components/UploadButton' +import { EmailAttachmentDto } from '@/pages/received/types/Types' +import { toast } from '@/components/Toast' + +interface EmailAttachmentsProps { + attachments: EmailAttachmentDto[] + onAttachmentsChange: (attachments: EmailAttachmentDto[]) => void +} + +const EmailAttachments: FC = ({ + attachments, + onAttachmentsChange +}) => { + const convertFileToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + const result = reader.result as string + const base64 = result.split(',')[1] + resolve(base64) + } + reader.onerror = reject + reader.readAsDataURL(file) + }) + } + + const handleFilesUpload = async (files: File[]) => { + try { + const newAttachments: EmailAttachmentDto[] = [] + + for (const file of files) { + const base64Content = await convertFileToBase64(file) + const attachment: EmailAttachmentDto = { + filename: file.name, + contentType: file.type, + encoding: 'base64', + contentDisposition: 'attachment', + content: base64Content + } + newAttachments.push(attachment) + } + + onAttachmentsChange([...attachments, ...newAttachments]) + } catch (error) { + console.error('Error converting files to base64:', error) + toast('خطا در آپلود فایل‌ها', 'error') + } + } + + const handleRemoveFile = (files: File[]) => { + const currentFileNames = files.map(f => f.name) + const updatedAttachments = attachments.filter(att => + currentFileNames.includes(att.filename || '') + ) + onAttachmentsChange(updatedAttachments) + } + + return ( +
+ +
+ ) +} + +export default EmailAttachments \ No newline at end of file diff --git a/src/components/newMessage/EmailEditor.tsx b/src/components/newMessage/EmailEditor.tsx new file mode 100644 index 0000000..9470ec5 --- /dev/null +++ b/src/components/newMessage/EmailEditor.tsx @@ -0,0 +1,39 @@ +import { FC } from 'react' +import ReactQuill from 'react-quill-new' + +interface EmailEditorProps { + value: string + onChange: (value: string) => void + placeholder?: string +} + +const EmailEditor: FC = ({ + value, + onChange, + placeholder = '' +}) => { + const modules = { + toolbar: [ + [{ header: '1' }, { header: '2' }, { font: [] }], + [{ list: 'ordered' }, { list: 'bullet' }], + ['bold', 'italic', 'underline'], + ['link', 'image'], + [{ align: [] }], + ['clean'] + ] + } + + return ( + + ) +} + +export default EmailEditor \ No newline at end of file diff --git a/src/components/newMessage/EmailFormActions.tsx b/src/components/newMessage/EmailFormActions.tsx new file mode 100644 index 0000000..00ad9fb --- /dev/null +++ b/src/components/newMessage/EmailFormActions.tsx @@ -0,0 +1,60 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/components/Button' +import Select from '@/components/Select' + +interface EmailFormActionsProps { + priority: string + onPriorityChange: (priority: string) => void + onSend: () => void + onSaveDraft: () => void + isSending: boolean + isSavingDraft: boolean +} + +const EmailFormActions: FC = ({ + priority, + onPriorityChange, + onSend, + onSaveDraft, + isSending, + isSavingDraft +}) => { + const { t } = useTranslation('global') + + const priorityOptions = [ + { value: 'high', label: 'بالا' }, + { value: 'medium', label: 'متوسط' }, + { value: 'low', label: 'پایین' }, + ] + + return ( +
+
+ +
+ + {showSuggestions && filteredSuggestions.length > 0 && ( +
+ {filteredSuggestions.map((suggestion, index) => ( +
selectSuggestion(suggestion)} + onMouseEnter={() => setSelectedSuggestionIndex(index)} + > +
+ {suggestion.name || suggestion.address} + {suggestion.name && ( + {suggestion.address} + )} +
+
+ ))} +
+ )} +
+ ) +} + +export default EmailInput \ No newline at end of file diff --git a/src/components/newMessage/NewMessage.tsx b/src/components/newMessage/NewMessage.tsx index e77340b..ec550cc 100644 --- a/src/components/newMessage/NewMessage.tsx +++ b/src/components/newMessage/NewMessage.tsx @@ -1,77 +1,56 @@ -import { FC, useState, useEffect, useRef } from 'react' +import { FC, useState, useEffect } from 'react' import { CloseCircle } from 'iconsax-react' import { useTranslation } from 'react-i18next' import { useSharedStore } from '@/shared/store/sharedStore' -import { useAuthStore } from '../../pages/auth/store/AuthStore' -import { useGetMessageDetail, useSendEmail } from '../../pages/received/hooks/useEmailData' -import { SendEmailDto } from '../../pages/received/types/Types' -import { useUpdateDraft, useSendDraft } from '../../pages/draft/hooks/useDraftData' -import Input from '../Input' -import ReactQuill from 'react-quill-new'; -import Button from '../Button' -import Select from '../Select' -import UploadButton from '../UploadButton' -import { toast } from '../Toast' - - +import Input from '@/components/Input' +import EmailInput from './EmailInput' +import EmailEditor from './EmailEditor' +import EmailAttachments from './EmailAttachments' +import EmailFormActions from './EmailFormActions' +import { useNewMessage } from './hooks/useNewMessage' const NewMessage: FC = () => { const { t } = useTranslation('global') - const [value, setValue] = useState(''); - const { setOpenNewMessage, openNewMessage, draftData, setDraftData, editingDraftId, setEditingDraftId } = useSharedStore() - const { email: userEmail } = useAuthStore() + const { openNewMessage, setOpenNewMessage } = useSharedStore() const [isClosing, setIsClosing] = useState(false) const [shouldRender, setShouldRender] = useState(false) const [isOpening, setIsOpening] = useState(false) - // Form state - const [to, setTo] = useState('') - const [toEmails, setToEmails] = useState([]) - const [subject, setSubject] = useState('') - const [priority, setPriority] = useState('') + const { + // Form state + toEmails, + subject, + content, + priority, + attachments, + emailSuggestions, - // Loading states - const [isSending, setIsSending] = useState(false) - const [isSavingDraft, setIsSavingDraft] = useState(false) + // Loading states + isSending, + isSavingDraft, - // Auto-add email functionality - const typingTimeoutRef = useRef(null) + // Form handlers + addEmail, + removeEmail, + setSubject, + setContent, + setPriority, + setAttachments, - // API hooks - const sendEmailMutation = useSendEmail() - const updateDraftMutation = useUpdateDraft() - const sendDraftMutation = useSendDraft() + // Search functions + searchEmailSuggestions, - // Get draft detail if draftData exists - const { data: draftDetail } = useGetMessageDetail( - draftData?.id.toString() || '', - draftData?.mailbox || '', - ) - - // Load draft data into form when draft detail is available - useEffect(() => { - if (draftDetail) { - // Extract emails from to array - const toEmailsList = draftDetail.to?.map(recipient => recipient.address) || [] - setToEmails(toEmailsList) - setSubject(draftDetail.subject || '') - - // Try to load HTML content if available, fallback to text or intro - const draftWithContent = draftDetail as unknown as { html?: string; text?: string; intro: string } - const content = ensureString(draftWithContent.html || draftWithContent.text || draftWithContent.intro) - setValue(content) - - // Clear draft data after loading - setDraftData(null) - } - }, [draftDetail, setDraftData]) + // Actions + handleSend, + handleSaveDraft, + resetForm + } = useNewMessage() useEffect(() => { if (openNewMessage) { setShouldRender(true) setIsClosing(false) setIsOpening(true) - // شروع انیمیشن باز شدن بعد از رندر setTimeout(() => { setIsOpening(false) }, 50) @@ -90,207 +69,25 @@ const NewMessage: FC = () => { setTimeout(() => { setOpenNewMessage(false) resetForm() - setDraftData(null) - setEditingDraftId(null) }, 300) } - const parseEmailAddresses = (emails: string[]) => { - return emails - .filter(email => email.length > 0) - .map(email => ({ address: email })) - } - - const isValidEmail = (email: string) => { - const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ - return emailRegex.test(email) - } - - const ensureString = (val: unknown): string => { - if (typeof val === 'string') return val - if (val === null || val === undefined) return '' - return String(val) - } - - const addEmailToList = (email: string) => { - if (!isValidEmail(email)) { - return - } - - if (toEmails.includes(email)) { - toast('این ایمیل قبلاً اضافه شده است', 'error') - return - } - - setToEmails([...toEmails, email]) - setTo('') - } - - const handleToChange = (e: React.ChangeEvent) => { - const newValue = e.target.value - setTo(newValue) - - // Clear existing timeout - if (typingTimeoutRef.current) { - clearTimeout(typingTimeoutRef.current) - } - - // Set new timeout to auto-add email after user stops typing - if (newValue.trim()) { - typingTimeoutRef.current = window.setTimeout(() => { - if (isValidEmail(newValue.trim())) { - addEmailToList(newValue.trim()) - } - }, 1500) // Wait 1.5 seconds after user stops typing + const handleSendWithClose = async () => { + const success = await handleSend() + if (success) { + handleClose() } } - const handleToBlur = () => { - // Clear timeout when input loses focus - if (typingTimeoutRef.current) { - clearTimeout(typingTimeoutRef.current) - } - - // Auto-add email if valid when input loses focus - if (to.trim() && isValidEmail(to.trim())) { - addEmailToList(to.trim()) + const handleSaveDraftWithClose = async () => { + const success = await handleSaveDraft() + if (success) { + handleClose() } } - const handleToKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && to.trim()) { - e.preventDefault() - - // Clear timeout when Enter is pressed - if (typingTimeoutRef.current) { - clearTimeout(typingTimeoutRef.current) - } - - addEmailToList(to.trim()) - } - } - - // Clean up timeout on component unmount - useEffect(() => { - return () => { - if (typingTimeoutRef.current) { - clearTimeout(typingTimeoutRef.current) - } - } - }, []) - - const removeEmail = (emailToRemove: string) => { - setToEmails(toEmails.filter(email => email !== emailToRemove)) - } - - const resetForm = () => { - setTo('') - setToEmails([]) - setSubject('') - setValue('') - setPriority('') - } - - const handleEmailAction = async (isDraft: boolean = false) => { - const isEditingDraft = !!editingDraftId - - if (!isDraft && (toEmails.length === 0 || !subject)) { - toast('لطفا تمام فیلدهای مورد نیاز را پر کنید', 'error') - return - } - - if (isDraft && toEmails.length === 0 && !subject && ensureString(value).trim() === '') { - toast('حداقل یکی از فیلدها باید پر باشد', 'error') - return - } - - // Set appropriate loading state - if (isDraft) { - setIsSavingDraft(true) - } else { - setIsSending(true) - } - - try { - let messageId = '' - if (isEditingDraft) { - // We're editing an existing draft - const draftUpdateData = { - to: parseEmailAddresses(toEmails), - subject, - html: ensureString(value), - text: ensureString(value).replace(/<[^>]*>/g, ''), - } - - if (isDraft) { - // Just update the draft - await updateDraftMutation.mutateAsync({ - messageId: editingDraftId.toString(), - draftData: draftUpdateData - }, { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onSuccess: (data: any) => { - messageId = data.data?.messageId - } - }) - } else { - // Update first, then send - await updateDraftMutation.mutateAsync({ - messageId: editingDraftId.toString(), - draftData: { - ...draftUpdateData, - isDraft: true, - uploadOnly: true - } - }, { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onSuccess: (data: any) => { - messageId = data.data?.messageId - } - }) - await sendDraftMutation.mutateAsync(messageId || editingDraftId.toString()) - toast('پیام با موفقیت ارسال شد', 'success') - } - handleClose() - } else { - // Creating new email/draft - const emailData: SendEmailDto = { - from: { address: userEmail || 'sender@example.com' }, - to: parseEmailAddresses(toEmails), - subject, - html: ensureString(value), - text: ensureString(value).replace(/<[^>]*>/g, ''), - ...(isDraft && { isDraft: true, uploadOnly: true }), - } - - await sendEmailMutation.mutateAsync(emailData, { - onSuccess: (data) => { - toast(data.message, 'success') - handleClose() - } - }) - } - } catch (error) { - console.error('Error in email action:', error) - toast('خطا در انجام عملیات', 'error') - } finally { - // Reset loading states - setIsSending(false) - setIsSavingDraft(false) - } - } - - const handleSend = () => handleEmailAction(false) - const handleSaveDraft = () => handleEmailAction(true) - - const priorityOptions = [ - { value: 'high', label: 'بالا' }, - { value: 'medium', label: 'متوسط' }, - { value: 'low', label: 'پایین' }, - ] return ( <> - {shouldRender && ( <>
{ -
-
- {/* Email Tags */} - {toEmails.map((email, index) => ( -
- {email} - removeEmail(email)} - /> -
- ))} - - {/* Input Field */} - -
+
+
@@ -354,60 +131,29 @@ const NewMessage: FC = () => { />
-
-
- -
+ -
-
-