recive email + save draft and list draft + skeleton email detail

This commit is contained in:
hamid zarghami
2025-07-10 11:06:36 +03:30
parent 4258f57662
commit df6e74c632
14 changed files with 540 additions and 228 deletions
+108 -60
View File
@@ -1,9 +1,9 @@
import { FC, useState, useEffect } from 'react'
import { FC, useState, useEffect, useRef } 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 { useSendEmail, useSaveDraft } from '../../pages/received/hooks/useEmailData'
import { useSendEmail } from '../../pages/received/hooks/useEmailData'
import { SendEmailDto } from '../../pages/received/types/Types'
import Input from '../Input'
import ReactQuill from 'react-quill-new';
@@ -11,6 +11,7 @@ import Button from '../Button'
import Select from '../Select'
import UploadButton from '../UploadButton'
import { toast } from '../Toast'
import { ErrorType } from '@/helpers/types'
const NewMessage: FC = () => {
@@ -28,10 +29,15 @@ const NewMessage: FC = () => {
const [subject, setSubject] = useState('')
const [priority, setPriority] = useState('')
// Loading states
const [isSending, setIsSending] = useState(false)
const [isSavingDraft, setIsSavingDraft] = useState(false)
// Auto-add email functionality
const typingTimeoutRef = useRef<number | null>(null)
// API hooks
const sendEmailMutation = useSendEmail()
const saveDraftMutation = useSaveDraft()
useEffect(() => {
if (openNewMessage) {
@@ -70,63 +76,92 @@ const NewMessage: FC = () => {
return emailRegex.test(email)
}
const addEmailToList = (email: string) => {
if (!isValidEmail(email)) {
return
}
if (toEmails.includes(email)) {
toast('این ایمیل قبلاً اضافه شده است', 'error')
return
}
setToEmails([...toEmails, email])
setTo('')
}
const handleToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 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 handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && to.trim()) {
e.preventDefault()
const email = to.trim()
if (!isValidEmail(email)) {
toast('لطفا یک ایمیل معتبر وارد کنید', 'error')
return
// Clear timeout when Enter is pressed
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
if (toEmails.includes(email)) {
toast('این ایمیل قبلاً اضافه شده است', 'error')
return
}
setToEmails([...toEmails, email])
setTo('')
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 handleSend = async () => {
if (toEmails.length === 0 || !subject) {
const handleEmailAction = async (isDraft: boolean = false) => {
if (!isDraft && (toEmails.length === 0 || !subject)) {
toast('لطفا تمام فیلدهای مورد نیاز را پر کنید', 'error')
return
}
const emailData: SendEmailDto = {
from: { address: userEmail || 'sender@example.com' },
to: parseEmailAddresses(toEmails),
subject,
html: value,
text: value.replace(/<[^>]*>/g, ''), // Strip HTML for text version
}
try {
await sendEmailMutation.mutateAsync(emailData)
toast('ایمیل با موفقیت ارسال شد', 'success')
handleClose()
// Reset form
setTo('')
setToEmails([])
setSubject('')
setValue('')
setPriority('')
} catch {
toast('خطا در ارسال ایمیل', 'error')
}
}
const handleSaveDraft = async () => {
if (toEmails.length === 0 && !subject && !value) {
if (isDraft && toEmails.length === 0 && !subject && !value) {
toast('حداقل یکی از فیلدها باید پر باشد', 'error')
return
}
@@ -136,25 +171,37 @@ const NewMessage: FC = () => {
to: parseEmailAddresses(toEmails),
subject,
html: value,
text: value.replace(/<[^>]*>/g, ''),
isDraft: true,
text: value.replace(/<[^>]*>/g, ''), // Strip HTML for text version
...(isDraft && { isDraft: true, uploadOnly: true }),
}
try {
await saveDraftMutation.mutateAsync(emailData)
toast('پیش‌نویس ذخیره شد', 'success')
handleClose()
// Reset form
setTo('')
setToEmails([])
setSubject('')
setValue('')
setPriority('')
} catch {
toast('خطا در ذخیره پیش‌نویس', 'error')
// Set appropriate loading state
if (isDraft) {
setIsSavingDraft(true)
} else {
setIsSending(true)
}
await sendEmailMutation.mutateAsync(emailData, {
onSuccess: (data) => {
toast(data.message, 'success')
handleClose()
resetForm()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
},
onSettled: () => {
// Reset loading states
setIsSending(false)
setIsSavingDraft(false)
}
})
}
const handleSend = () => handleEmailAction(false)
const handleSaveDraft = () => handleEmailAction(true)
const priorityOptions = [
{ value: 'high', label: 'بالا' },
{ value: 'medium', label: 'متوسط' },
@@ -170,7 +217,7 @@ const NewMessage: FC = () => {
}`}
onClick={handleClose}
/>
<div className={`fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 xl:left-8 xl:right-auto xl:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0'
<div className={`fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0'
}`}>
{/* Header */}
<div className='flex justify-between items-center mb-6 md:mb-8'>
@@ -209,7 +256,8 @@ const NewMessage: FC = () => {
{/* Input Field */}
<input
value={to}
onChange={(e) => setTo(e.target.value)}
onChange={handleToChange}
onBlur={handleToBlur}
onKeyDown={handleToKeyDown}
placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""}
className="flex-1 min-w-32 bg-transparent outline-none"
@@ -263,13 +311,13 @@ const NewMessage: FC = () => {
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
label={t('new_message.draft')}
onClick={handleSaveDraft}
loading={saveDraftMutation.isPending}
loading={isSavingDraft}
/>
<Button
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
label={t('new_message.send')}
onClick={handleSend}
loading={sendEmailMutation.isPending}
loading={isSending}
/>
</div>
</div>