download email + search

This commit is contained in:
hamid zarghami
2025-07-30 10:46:34 +03:30
parent e35634a0c2
commit f669955ab5
5 changed files with 147 additions and 13 deletions
+31 -1
View File
@@ -11,6 +11,7 @@ import MessageFavorite from './MessageFavorite'
import MessageTrash from './MessageTrash' import MessageTrash from './MessageTrash'
import { MessageDetail } from '../types/Types' import { MessageDetail } from '../types/Types'
import { sanitizeEmailHTML, detectTextDirection } from '@/config/func' import { sanitizeEmailHTML, detectTextDirection } from '@/config/func'
import { useDownloadEmail } from '../hooks/useEmailData'
const Header: FC<{ const Header: FC<{
mailBoxName: MailboxEnum, mailBoxName: MailboxEnum,
@@ -20,7 +21,7 @@ const Header: FC<{
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation() const { t } = useTranslation()
const { mutate: downloadEmail } = useDownloadEmail()
const handlePrint = () => { const handlePrint = () => {
if (!messageDetail) return if (!messageDetail) return
@@ -92,6 +93,34 @@ const Header: FC<{
} }
} }
const handleDownloadEmail = () => {
if (!messageDetail) return
downloadEmail({ messageId: Number(messageDetail.id), mailbox: messageDetail.mailbox }, {
onSuccess: (data) => {
try {
const url = window.URL.createObjectURL(data)
const link = document.createElement('a')
link.href = url
const fileName = (messageDetail.subject || `email-${messageDetail.id}`)
.replace(/[<>:"/\\|?*]/g, '') // حذف کاراکترهای غیرمجاز
.substring(0, 100) // محدود کردن طول نام فایل
link.download = `${fileName}.eml`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('خطا در دانلود فایل:', error)
alert('خطا در دانلود فایل رخ داد')
}
},
onError: (error) => {
console.error('خطا در دریافت فایل:', error)
alert('خطا در دریافت فایل از سرور رخ داد')
}
})
}
return ( return (
<div className='flex justify-between items-center border-b border-border pb-6'> <div className='flex justify-between items-center border-b border-border pb-6'>
<div className='flex flex-1 lg:gap-4'> <div className='flex flex-1 lg:gap-4'>
@@ -111,6 +140,7 @@ const Header: FC<{
<Button <Button
variant='secondary' variant='secondary'
className='lg:block hidden' className='lg:block hidden'
onClick={handleDownloadEmail}
> >
<div className='flex gap-2 items-center xl:px-5'> <div className='flex gap-2 items-center xl:px-5'>
<DocumentDownload size={20} color='black' /> <DocumentDownload size={20} color='black' />
@@ -28,6 +28,7 @@ export const useGetInbox = (query: MessageListQueryDto) => {
queryFn: () => api.getInbox(query), queryFn: () => api.getInbox(query),
staleTime: 0, // Always fetch fresh data staleTime: 0, // Always fetch fresh data
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
enabled: query.enabled ?? true
}); });
}; };
@@ -137,3 +138,9 @@ export const useMarkAllRead = () => {
mutationFn: () => api.markAllRead(), mutationFn: () => api.markAllRead(),
}); });
}; };
export const useDownloadEmail = () => {
return useMutation({
mutationFn: ({ messageId, mailbox }: { messageId: number; mailbox: string }) => api.downloadEmail(messageId, mailbox),
});
};
@@ -168,3 +168,13 @@ export const markAllRead = async () => {
); );
return data; return data;
}; };
export const downloadEmail = async (messageId: number, mailbox: string) => {
const {
data,
} = await axios.get(
`/email/messages/${messageId}/download?mailbox=${mailbox}`,
{ responseType: "blob" }
);
return data;
};
+1
View File
@@ -66,6 +66,7 @@ export interface MessageListQueryDto {
unseen?: boolean; unseen?: boolean;
flagged?: boolean; flagged?: boolean;
order?: "asc" | "desc"; order?: "asc" | "desc";
enabled?: boolean;
} }
// Updated types based on actual API response // Updated types based on actual API response
+92 -6
View File
@@ -10,6 +10,7 @@ import { Paths } from '@/utils/Paths'
import SideBarItem from './SideBarItem' import SideBarItem from './SideBarItem'
import AvatarImage from '@/assets/images/avatar_image.png' import AvatarImage from '@/assets/images/avatar_image.png'
import { useGetProfile } from '@/pages/profile/hooks/useProfileData' import { useGetProfile } from '@/pages/profile/hooks/useProfileData'
import { useGetInbox } from '@/pages/received/hooks/useEmailData'
const Header: FC = () => { const Header: FC = () => {
@@ -20,6 +21,11 @@ const Header: FC = () => {
const [inputFocused, setInputFocused] = useState(false) const [inputFocused, setInputFocused] = useState(false)
const [showMobileSearch, setShowMobileSearch] = useState(false) const [showMobileSearch, setShowMobileSearch] = useState(false)
const { pathname } = useLocation() const { pathname } = useLocation()
const { data: inboxData } = useGetInbox({
limit: 10,
search: search,
enabled: !!search,
})
const { t } = useTranslation('global') const { t } = useTranslation('global')
const { setOpenSidebar, openSidebar } = useSharedStore() const { setOpenSidebar, openSidebar } = useSharedStore()
@@ -33,6 +39,19 @@ const Header: FC = () => {
} }
} }
const clearSearch = () => {
setSearch('')
setShowSearchResults(false)
setInputFocused(false)
}
const clearSearchMobile = () => {
setSearch('')
setShowSearchResults(false)
setInputFocused(false)
setShowMobileSearch(false)
}
const handleInputFocus = () => { const handleInputFocus = () => {
setInputFocused(true) setInputFocused(true)
if (search) { if (search) {
@@ -66,8 +85,7 @@ const Header: FC = () => {
} }
}, 100) }, 100)
} else { } else {
setSearch('') clearSearch()
setShowSearchResults(false)
} }
} }
@@ -86,10 +104,10 @@ const Header: FC = () => {
mobileSearchContainer && !mobileSearchContainer.contains(e.target as Node) && mobileSearchContainer && !mobileSearchContainer.contains(e.target as Node) &&
mobileSearchInput && !mobileSearchInput.contains(e.target as Node); mobileSearchInput && !mobileSearchInput.contains(e.target as Node);
if ((searchContainer && searchInput && isClickOutsideDesktopSearch) || if (searchContainer && searchInput && isClickOutsideDesktopSearch) {
(mobileSearchContainer && mobileSearchInput && isClickOutsideMobileSearch)) { clearSearch()
setShowSearchResults(false) } else if (mobileSearchContainer && mobileSearchInput && isClickOutsideMobileSearch) {
setInputFocused(false) clearSearchMobile()
} }
} }
@@ -122,9 +140,43 @@ const Header: FC = () => {
id="search-dropdown-container" id="search-dropdown-container"
className='absolute z-20 mt-1 w-full bg-white rounded-xl shadow-lg max-h-80 overflow-y-auto' className='absolute z-20 mt-1 w-full bg-white rounded-xl shadow-lg max-h-80 overflow-y-auto'
> >
{inboxData?.data?.results && inboxData.data.results.length > 0 ? (
<div className='py-2'>
{inboxData.data.results.slice(0, 10).map((message) => (
<Link
key={message.id}
to={`/mail/${message.id}/${message.mailbox}`}
className='flex items-center gap-3 px-4 py-2 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0'
onClick={clearSearch}
>
<div className='flex-1 min-w-0'>
<div className='flex items-center gap-2 mb-1'>
{!message.seen && (
<div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" />
)}
<span className='text-sm font-medium text-gray-900 truncate'>
{message.from.name || message.from.address}
</span>
</div>
<div className='text-sm text-gray-800 font-medium truncate mb-1'>
{message.subject || 'بدون موضوع'}
</div>
<div className='text-xs text-gray-500 truncate'>
{message.intro}
</div>
</div>
<div className='text-xs text-gray-400 flex-shrink-0'>
{new Date(message.date).toLocaleDateString('fa-IR')}
</div>
</Link>
))}
</div>
) : (
<div className='p-4 text-center text-sm text-description'> <div className='p-4 text-center text-sm text-description'>
نتیجهای یافت نشد نتیجهای یافت نشد
</div> </div>
)}
</div> </div>
)} )}
</div> </div>
@@ -167,9 +219,43 @@ const Header: FC = () => {
id="mobile-search-dropdown-container" id="mobile-search-dropdown-container"
className='fixed right-4 left-4 top-16 z-1 bg-white rounded-xl shadow-lg max-h-[calc(100vh-80px)] overflow-y-auto' className='fixed right-4 left-4 top-16 z-1 bg-white rounded-xl shadow-lg max-h-[calc(100vh-80px)] overflow-y-auto'
> >
{inboxData?.data?.results && inboxData.data.results.length > 0 ? (
<div className='py-2'>
{inboxData.data.results.slice(0, 10).map((message) => (
<Link
key={message.id}
to={`/mail/${message.id}/${message.mailbox}`}
className='flex items-center gap-3 px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0'
onClick={clearSearchMobile}
>
<div className='flex-1 min-w-0'>
<div className='flex items-center gap-2 mb-1'>
{!message.seen && (
<div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" />
)}
<span className='text-sm font-medium text-gray-900 truncate'>
{message.from.name || message.from.address}
</span>
</div>
<div className='text-sm text-gray-800 font-medium truncate mb-1'>
{message.subject || 'بدون موضوع'}
</div>
<div className='text-xs text-gray-500 truncate'>
{message.intro}
</div>
</div>
<div className='text-xs text-gray-400 flex-shrink-0'>
{new Date(message.date).toLocaleDateString('fa-IR')}
</div>
</Link>
))}
</div>
) : (
<div className='p-4 text-center text-sm text-description'> <div className='p-4 text-center text-sm text-description'>
نتیجهای یافت نشد نتیجهای یافت نشد
</div> </div>
)}
</div> </div>
)} )}