diff --git a/.env b/.env index a5473ac..42f1816 100644 --- a/.env +++ b/.env @@ -6,8 +6,8 @@ VITE_DANAK_BASE_URL ='https://api.danakcorp.com' VITE_SOCKET_URL = 'wss://dmail-api.danakcorp.com/email' # VITE_SOCKET_URL = 'ws://192.168.1.118:4000/email' -VITE_BASE_URL = 'https://dmail-api.danakcorp.com' -# VITE_BASE_URL = 'http://192.168.1.118:4000' +# VITE_BASE_URL = 'https://dmail-api.danakcorp.com' +VITE_BASE_URL = 'http://192.168.1.118:4000' VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e' diff --git a/src/components/newMessage/EmailActions.tsx b/src/components/newMessage/EmailActions.tsx new file mode 100644 index 0000000..68575ff --- /dev/null +++ b/src/components/newMessage/EmailActions.tsx @@ -0,0 +1,45 @@ +import { FC } from 'react' +import { EmojiHappy } from 'iconsax-react' +import Reply from './Reply' +import Forward from './Forward' +import { MessageDetail } from '@/pages/received/types/Types' + +interface EmailActionsProps { + messageDetail: MessageDetail + className?: string + showReactions?: boolean +} + +const EmailActions: FC = ({ + messageDetail, + className = '', + showReactions = true +}) => { + return ( +
+
+ {showReactions && ( + + )} + + { }} + /> + + { }} + /> +
+
+ ) +} + +export default EmailActions \ No newline at end of file diff --git a/src/components/newMessage/EmailEditor.tsx b/src/components/newMessage/EmailEditor.tsx index 9470ec5..6d43999 100644 --- a/src/components/newMessage/EmailEditor.tsx +++ b/src/components/newMessage/EmailEditor.tsx @@ -12,20 +12,10 @@ const EmailEditor: FC = ({ onChange, placeholder = '' }) => { - const modules = { - toolbar: [ - [{ header: '1' }, { header: '2' }, { font: [] }], - [{ list: 'ordered' }, { list: 'bullet' }], - ['bold', 'italic', 'underline'], - ['link', 'image'], - [{ align: [] }], - ['clean'] - ] - } + return ( void onSend: () => void - onSaveDraft: () => void isSending: boolean - isSavingDraft: boolean + onClose?: () => void + onSaveDraft?: () => void + isSavingDraft?: boolean + priority?: string + onPriorityChange?: (priority: string) => void } const EmailFormActions: FC = ({ - priority, - onPriorityChange, onSend, - onSaveDraft, isSending, - isSavingDraft + onClose, + onSaveDraft, + isSavingDraft = false, + priority, + onPriorityChange }) => { const { t } = useTranslation('global') @@ -30,22 +32,40 @@ const EmailFormActions: FC = ({ return (
-
- onPriorityChange(e.target.value)} + /> +
+ )} +
- + +
+
+ + {/* Forward Content */} +
+ +
+ {/* To Field */} +
+ { + if (!toEmails.includes(email)) { + setToEmails([...toEmails, email]) + } + }} + onRemoveEmail={(email) => { + setToEmails(toEmails.filter(e => e !== email)) + }} + suggestions={[]} + /> +
+ + {/* Subject Field */} +
+
+ موضوع: + + {originalMessage.subject.startsWith('Fwd:') + ? originalMessage.subject + : `Fwd: ${originalMessage.subject}`} + +
+
+ + {/* Editor */} +
+ +
+ + {/* Actions */} + +
+
+ + ) +} + +export default Forward \ No newline at end of file diff --git a/src/components/newMessage/NewMessage.tsx b/src/components/newMessage/NewMessage.tsx index ec550cc..e5a748f 100644 --- a/src/components/newMessage/NewMessage.tsx +++ b/src/components/newMessage/NewMessage.tsx @@ -95,7 +95,7 @@ const NewMessage: FC = () => { }`} onClick={handleClose} /> -
{/* Header */}
diff --git a/src/components/newMessage/Reply.tsx b/src/components/newMessage/Reply.tsx new file mode 100644 index 0000000..6e67a27 --- /dev/null +++ b/src/components/newMessage/Reply.tsx @@ -0,0 +1,162 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { ArrowDown2, CloseSquare } from 'iconsax-react' +import AvatarImage from '@/assets/images/avatar.svg' +import AnswerIcon from '@/assets/images/answer.svg' +import EmailEditor from './EmailEditor' +import EmailFormActions from './EmailFormActions' +import { useReply } from './hooks/useReply' +import { MessageDetail } from '@/pages/received/types/Types' +import { toast } from '@/components/Toast' + +interface ReplyProps { + originalMessage: MessageDetail + onClose?: () => void + onExpand?: () => void + className?: string + showInitialButton?: boolean +} + +const Reply: FC = ({ + originalMessage, + onClose, + onExpand, + className = '', + showInitialButton = true +}) => { + const { t } = useTranslation() + + const { + content, + isExpanded, + isSending, + setContent, + expandReply, + collapseReply, + handleSendReply, + handleSaveDraft + } = useReply({ + originalMessage, + onSuccess: () => { + onClose?.() + } + }) + + const handleExpand = () => { + expandReply() + onExpand?.() + } + + + const handleClose = async () => { + // اگر محتوایی وارد شده، پیش‌نویس ذخیره کن + if (content.trim()) { + await handleSaveDraft() + toast('پیش‌نویس ذخیره شد', 'success') + } + + // بسته کردن و برگشت به حالت قبل + collapseReply() + onClose?.() // این خط مهمه تا parent component بفهمه + } + + // اگر showInitialButton true باشه و هنوز expand نشده، فقط دکمه نشون بده + if (showInitialButton && !isExpanded) { + return ( +
+
+
+ +
{t('mail.answer')}
+
+
+
+ ) + } + + const handleSend = () => { + handleSendReply(originalMessage?.html[0]) + collapseReply() + } + + // در غیر این صورت editor رو نشون بده + // (یا showInitialButton false هست یا isExpanded true هست) + + return ( +
+ {/* Reply Header */} +
+
+ + پاسخ به {originalMessage.from.name || originalMessage.from.address} +
+
+ + +
+
+ + {/* Reply Content */} +
+ +
+ {/* To Field */} +
+
+ به: + {originalMessage.from.address} +
+
+ + {/* Subject Field */} +
+
+ موضوع: + + {originalMessage.subject.startsWith('Re:') + ? originalMessage.subject + : `Re: ${originalMessage.subject}`} + +
+
+ + {/* Editor */} +
+ +
+ + {/* Actions */} + +
+
+
+ ) +} + +export default Reply \ No newline at end of file diff --git a/src/components/newMessage/hooks/useReply.ts b/src/components/newMessage/hooks/useReply.ts new file mode 100644 index 0000000..60620b3 --- /dev/null +++ b/src/components/newMessage/hooks/useReply.ts @@ -0,0 +1,137 @@ +import { useState } from "react"; +import { useAuthStore } from "@/pages/auth/store/AuthStore"; +import { useSendEmail } from "@/pages/received/hooks/useEmailData"; +import { SendEmailDto } from "@/pages/received/types/Types"; +import { MessageDetail } from "@/pages/received/types/Types"; +import { toast } from "@/components/Toast"; + +interface UseReplyProps { + originalMessage: MessageDetail; + onSuccess?: () => void; + onError?: (error: string) => void; +} + +export const useReply = ({ + originalMessage, + onSuccess, + onError, +}: UseReplyProps) => { + const { email: userEmail } = useAuthStore(); + const [content, setContent] = useState(""); + const [isExpanded, setIsExpanded] = useState(false); + + // API hooks + const sendEmailMutation = useSendEmail(); + + const resetForm = () => { + setContent(""); + setIsExpanded(false); + }; + + const handleSendReply = async (html: string) => { + if (!content.trim()) { + toast("لطفا متن پیام را وارد کنید", "error"); + return false; + } + + if (!originalMessage) { + toast("اطلاعات پیام یافت نشد", "error"); + return false; + } + + // Format date to Persian + const originalDate = new Date(originalMessage.date); + const persianDate = new Intl.DateTimeFormat("fa-IR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(originalDate); + + const htmlContent = + content + + "

" + + `
در تاریخ ${persianDate} ${originalMessage.from.name} <${originalMessage.from.address}> نوشت:
` + + "

" + + `
${html}
`; + + const replyData: SendEmailDto = { + from: { address: userEmail || "sender@example.com" }, + to: [ + { + address: originalMessage.from.address, + name: originalMessage.from.name, + }, + ], + replyTo: { + name: originalMessage.from.name, + address: originalMessage.from.address, + }, + subject: originalMessage.subject.startsWith("Re:") + ? originalMessage.subject + : `Re: ${originalMessage.subject}`, + html: htmlContent, + text: content.replace(/<[^>]*>/g, ""), + }; + + try { + await sendEmailMutation.mutateAsync(replyData); + toast("پاسخ با موفقیت ارسال شد", "success"); + resetForm(); + onSuccess?.(); + return true; + } catch { + const errorMessage = "خطا در ارسال پاسخ"; + toast(errorMessage, "error"); + onError?.(errorMessage); + return false; + } + }; + + const handleSaveDraft = async () => { + if (!content.trim()) { + toast("متن پیام خالی است", "error"); + return false; + } + + try { + // TODO: Implement save draft functionality + toast("پیش‌نویس ذخیره شد", "success"); + return true; + } catch { + const errorMessage = "خطا در ذخیره پیش‌نویس"; + toast(errorMessage, "error"); + onError?.(errorMessage); + return false; + } + }; + + const expandReply = () => { + setIsExpanded(true); + }; + + const collapseReply = () => { + setIsExpanded(false); + resetForm(); + }; + + return { + // State + content, + isExpanded, + isSending: sendEmailMutation.isPending, + + // Handlers + setContent, + expandReply, + collapseReply, + handleSendReply, + handleSaveDraft, + resetForm, + + // Original message data + originalMessage, + }; +}; diff --git a/src/index.css b/src/index.css index 01a6e8d..faed859 100644 --- a/src/index.css +++ b/src/index.css @@ -348,3 +348,6 @@ textarea::placeholder { backdrop-filter: blur(44px); } +strong { + font-weight: bold; +} diff --git a/src/langs/fa.json b/src/langs/fa.json index 2f8f144..b2dd9c8 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -23,6 +23,9 @@ "required": "این فیلد اجباری است", "invalid_email": "فرمت ایمیل نامعتبر است" }, + "common": { + "close": "بستن" + }, "received": { "title": "دریافتی ها", "from_date": "از تاریخ", @@ -140,7 +143,8 @@ "FAVORITE": "نشان‌شده‌ها", "Junk": "هرزنامه‌ها", "SENT": "ارسال شده‌ها", - "TRASH": "سطل زباله" + "TRASH": "سطل زباله", + "Sent Mail": "ارسال شده ها" }, "auth": { "welcome": "خوش آمدید", diff --git a/src/pages/received/Detail.tsx b/src/pages/received/Detail.tsx index 2ddfe62..93cd6a5 100644 --- a/src/pages/received/Detail.tsx +++ b/src/pages/received/Detail.tsx @@ -4,112 +4,26 @@ import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' import AvatarImage from '@/assets/images/avatar.svg' import { DocumentDownload, EmojiHappy } from 'iconsax-react' -import ForwardIcon from '@/assets/images/forward.svg' -import AnswerIcon from '@/assets/images/answer.svg' import Header from './Components/Header' -import ReactQuill from 'react-quill-new'; -import Select from '@/components/Select' -import { useGetMessageDetail, useSendEmail, useDownloadAttachment } from './hooks/useEmailData' -import { useAuthStore } from '../auth/store/AuthStore' -import { SendEmailDto } from './types/Types' +import { useGetMessageDetail, useDownloadAttachment } from './hooks/useEmailData' import { toast } from '@/components/Toast' import { formatDate, sanitizeEmailHTML, detectTextDirection } from '@/config/func' import SkeletonDetail from './Components/SkeletonDetail' import { useEmailActions } from '@/hooks/useEmailActions' import { MailboxEnum } from './enum/Enum' +import Reply from '@/components/newMessage/Reply' +import Forward from '@/components/newMessage/Forward' const DetailEmail: FC = () => { const { t } = useTranslation() const emailActions = useEmailActions() const { id, mailbox } = useParams<{ id: string, mailbox: string }>() - const { email: userEmail } = useAuthStore() - const [showAnswer, setShowAnswer] = useState(false) - const [value, setValue] = useState('') - const [priority, setPriority] = useState('') + const [activeAction, setActiveAction] = useState<'reply' | 'forward' | null>(null) // API hooks const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', mailbox || '') - const sendEmailMutation = useSendEmail() const downloadAttachmentMutation = useDownloadAttachment() - // const saveDraftMutation = useSaveDraft() - - const priorityOptions = [ - { value: 'high', label: 'بالا' }, - { value: 'medium', label: 'متوسط' }, - { value: 'low', label: 'پایین' }, - ] - - const handleSendReply = async () => { - if (!value.trim()) { - toast('لطفا متن پیام را وارد کنید', 'error') - return - } - - if (!messageDetail) { - toast('اطلاعات پیام یافت نشد', 'error') - return - } - - const replyData: SendEmailDto = { - from: { address: userEmail || 'sender@example.com' }, - to: [{ address: messageDetail.from.address, name: messageDetail.from.name }], - subject: `Re: ${messageDetail.subject}`, - html: value, - text: value.replace(/<[^>]*>/g, ''), - reference: { - inReplyTo: messageDetail.messageId, - originalMessageId: messageDetail.id.toString() - } - } - - try { - await sendEmailMutation.mutateAsync(replyData) - toast('پاسخ با موفقیت ارسال شد', 'success') - setShowAnswer(false) - setValue('') - setPriority('') - } catch { - toast('خطا در ارسال پاسخ', 'error') - } - } - - const handleSaveDraft = async () => { - if (!value.trim()) { - toast('متن پیام خالی است', 'error') - return - } - - if (!messageDetail) { - toast('اطلاعات پیام یافت نشد', 'error') - return - } - - // const draftData: SendEmailDto = { - // from: { address: userEmail || 'sender@example.com' }, - // to: [{ address: messageDetail.from.address, name: messageDetail.from.name }], - // subject: `Re: ${messageDetail.subject}`, - // html: value, - // text: value.replace(/<[^>]*>/g, ''), - // isDraft: true, - // reference: { - // inReplyTo: messageDetail.messageId, - // originalMessageId: messageDetail.id.toString() - // } - // } - - try { - // await saveDraftMutation.mutateAsync(draftData) - toast('پیش‌نویس ذخیره شد', 'success') - } catch { - toast('خطا در ذخیره پیش‌نویس', 'error') - } - } - - const handleForward = () => { - // TODO: Implement forward functionality - toast('قابلیت ارسال به دیگران به زودی اضافه خواهد شد', 'info') - } const handleDownloadAttachment = async (attachmentId: string, filename: string) => { if (!id) { @@ -245,78 +159,47 @@ const DetailEmail: FC = () => { )}
- { - !showAnswer ? -
-
- + {/* Email Actions */} + {activeAction === null && ( +
+
+ -
setShowAnswer(true)} className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'> - -
{t('mail.answer')}
-
+ setActiveAction(null)} + onExpand={() => setActiveAction('reply')} + /> -
- -
{t('mail.forward')}
-
-
+ setActiveAction(null)} + onExpand={() => setActiveAction('forward')} + />
- : -
- -
-
- {t('mail.message')} -
- +
+ )} -
-
-
-