download email + search
This commit is contained in:
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user