Files
dmail-front/src/shared/Header.tsx
T
hamid zarghami d0496c2bd7 notification
2025-08-23 16:34:11 +03:30

389 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FC, useEffect, useState } from 'react'
import Input from '../components/Input'
import { ArrowDown2, CloseCircle, HambergerMenu, Logout, MessageQuestion, ProfileCircle, SearchNormal, Setting2 } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import Notifications from '../pages/notification/Notification'
import { useSharedStore } from './store/sharedStore'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { Link, useLocation } from 'react-router-dom'
import { Paths } from '@/utils/Paths'
import SideBarItem from './SideBarItem'
import AvatarImage from '@/assets/images/avatar_image.png'
import { useGetProfile } from '@/pages/profile/hooks/useProfileData'
import { useGetInbox } from '@/pages/received/hooks/useEmailData'
import ThemeToggle from '@/components/ThemeToggle'
import { getIconColor } from '@/utils/colorUtils'
import RowActionsDropdown from '@/components/RowActionsDropdown'
const Header: FC = () => {
const [popoverKey, setPopoverKey] = useState(0);
const { data } = useGetProfile()
const [search, setSearch] = useState<string>('')
const [showSearchResults, setShowSearchResults] = useState(false)
const [inputFocused, setInputFocused] = useState(false)
const [showMobileSearch, setShowMobileSearch] = useState(false)
const { pathname } = useLocation()
const { data: inboxData } = useGetInbox({
limit: 10,
search: search,
enabled: !!search,
})
const { t } = useTranslation('global')
const { setOpenSidebar, openSidebar, setOpenReportBug, setEmailWidth } = useSharedStore()
const [popoverButtonRef, setPopoverButtonRef] = useState<HTMLDivElement | null>(null)
const [popoverButtonWidth, setPopoverButtonWidth] = useState<number>(0)
const handleSearchChange = (value: string) => {
setSearch(value)
if (value) {
setShowSearchResults(true)
} else {
setShowSearchResults(false)
}
}
const clearSearch = () => {
setSearch('')
setShowSearchResults(false)
setInputFocused(false)
}
const clearSearchMobile = () => {
setSearch('')
setShowSearchResults(false)
setInputFocused(false)
setShowMobileSearch(false)
}
const handleInputFocus = () => {
setInputFocused(true)
if (search) {
setShowSearchResults(true)
}
}
const handleInputBlur = (e: React.FocusEvent) => {
const searchContainer = document.getElementById('search-dropdown-container')
const mobileSearchContainer = document.getElementById('mobile-search-dropdown-container')
if ((searchContainer && searchContainer.contains(e.relatedTarget as Node)) ||
(mobileSearchContainer && mobileSearchContainer.contains(e.relatedTarget as Node))) {
return
}
setInputFocused(false)
setTimeout(() => {
setShowSearchResults(false)
}, 200)
}
const toggleMobileSearch = () => {
setShowMobileSearch(prev => !prev)
if (!showMobileSearch) {
setTimeout(() => {
const mobileSearchInput = document.getElementById('mobile-search-input')
if (mobileSearchInput) {
(mobileSearchInput as HTMLInputElement).focus()
setInputFocused(true)
}
}, 100)
} else {
clearSearch()
}
}
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const searchContainer = document.getElementById('search-dropdown-container')
const mobileSearchContainer = document.getElementById('mobile-search-dropdown-container')
const searchInput = document.getElementById('search-input-container')
const mobileSearchInput = document.getElementById('mobile-search-input-container')
const isClickOutsideDesktopSearch =
searchContainer && !searchContainer.contains(e.target as Node) &&
searchInput && !searchInput.contains(e.target as Node);
const isClickOutsideMobileSearch =
mobileSearchContainer && !mobileSearchContainer.contains(e.target as Node) &&
mobileSearchInput && !mobileSearchInput.contains(e.target as Node);
if (searchContainer && searchInput && isClickOutsideDesktopSearch) {
clearSearch()
} else if (mobileSearchContainer && mobileSearchInput && isClickOutsideMobileSearch) {
clearSearchMobile()
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
useEffect(() => {
setPopoverKey((prevKey) => prevKey + 1)
}, [pathname])
useEffect(() => {
if (popoverButtonRef) {
const rect = popoverButtonRef.getBoundingClientRect();
setPopoverButtonWidth(rect.width);
console.log('popoverButtonRef width:', rect.width);
console.log('popoverButtonRef height:', rect.height);
console.log('popoverButtonRef dimensions:', { width: rect.width, height: rect.height });
console.log('Stored width in state:', popoverButtonWidth);
setEmailWidth(rect.width);
}
}, [popoverButtonRef])
return (
<div className='fixed z-1 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-surface justify-between rounded-[32px] xl:w-[calc(100%-305px)] border border-surface'>
{/* Desktop Search */}
<div className='min-w-[270px] hidden xl:block relative'>
<div id="search-input-container">
<Input
variant='search'
placeholder={t('header.search')}
onChangeSearchFinal={handleSearchChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
</div>
{showSearchResults && search && inputFocused && (
<div
id="search-dropdown-container"
className='absolute z-20 mt-1 w-full bg-surface border border-surface 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-accent cursor-pointer border-b border-border 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-foreground truncate'>
{message.from.name || message.from.address}
</span>
</div>
<div className='text-sm text-foreground font-medium truncate mb-1'>
{message.subject || 'بدون موضوع'}
</div>
<div className='text-xs text-muted-foreground truncate'>
{message.intro}
</div>
</div>
<div className='text-xs text-muted-foreground flex-shrink-0'>
{new Date(message.date).toLocaleDateString('fa-IR')}
</div>
</Link>
))}
</div>
) : (
<div className='p-4 text-center text-sm text-muted-foreground'>
نتیجهای یافت نشد
</div>
)}
</div>
)}
</div>
{/* Mobile Layout */}
<div className={`${showMobileSearch ? 'w-full' : ''} items-center`}>
{!showMobileSearch && (
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
<HambergerMenu size={24} color={getIconColor('primary')} />
</div>
)}
{/* Full width mobile search when active */}
{showMobileSearch && (
<div className='flex w-full items-center gap-2'>
<div className='flex-1' id="mobile-search-input-container">
<Input
id="mobile-search-input"
variant='search'
placeholder={t('header.search')}
onChangeSearchFinal={handleSearchChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
autoFocus
/>
</div>
<div
onClick={toggleMobileSearch}
className='p-2 flex-shrink-0 flex items-center justify-center'
>
<CloseCircle size={20} color={getIconColor('muted')} />
</div>
</div>
)}
</div>
{/* Mobile Search Results */}
{showMobileSearch && showSearchResults && search && (
<div
id="mobile-search-dropdown-container"
className='fixed right-4 left-4 top-16 z-1 bg-surface border border-surface 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-accent cursor-pointer border-b border-border 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-foreground truncate'>
{message.from.name || message.from.address}
</span>
</div>
<div className='text-sm text-foreground font-medium truncate mb-1'>
{message.subject || 'بدون موضوع'}
</div>
<div className='text-xs text-muted-foreground truncate'>
{message.intro}
</div>
</div>
<div className='text-xs text-muted-foreground flex-shrink-0'>
{new Date(message.date).toLocaleDateString('fa-IR')}
</div>
</Link>
))}
</div>
) : (
<div className='p-4 text-center text-sm text-muted-foreground'>
نتیجهای یافت نشد
</div>
)}
</div>
)}
<div className={`flex xl:gap-6 gap-4 items-center ${showMobileSearch ? 'hidden xl:flex' : ''}`}>
{/* Mobile search icon */}
{!showMobileSearch && (
<div onClick={toggleMobileSearch} className='xl:hidden block'>
<SearchNormal size={20} color={getIconColor('primary')} />
</div>
)}
<RowActionsDropdown
actions={[
{
label: 'راهنما',
onClick: () => window.open(
import.meta.env.VITE_HELP_URL + `/${import.meta.env.VITE_SERVICE_ID}`,
'_blank'
),
},
{
label: 'گزارش اشکالات',
onClick: () => setOpenReportBug(true),
},
]}
trigger={<MessageQuestion size={20} color={getIconColor('primary')} />}
topOffset={20}
leftOffset={70}
topOffsetMobile={10}
leftOffsetMobile={60}
/>
<ThemeToggle />
<Notifications />
{
data && (
<Popover className="relative" key={popoverKey}>
<div ref={setPopoverButtonRef}>
<PopoverButton >
<div className='flex gap-2 items-center mt-2.5'>
<div className='size-6 rounded-full bg-description overflow-hidden'>
<img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
</div>
<div className='xl:flex hidden gap-1 items-center'>
<div className='text-xs'>
{data?.data?.user?.emailAddress}
</div>
<ArrowDown2 size={14} color={getIconColor('muted')} />
</div>
</div>
</PopoverButton>
</div>
<PopoverPanel style={{ minHeight: window.innerWidth < 1140 ? window.innerHeight : undefined }} anchor="bottom" className="flex xl:ml-6 overflow-auto flex-col gap-3 bg-surface border border-surface boxShadow xl:mt-7 z-30 py-4 text-xs rounded-2.5 xl:w-[300px] -mt-[50px] w-full fixed xl:h-fit h-full shadow-md">
<div className='absolute xl:hidden top-6 left-6'>
<CloseCircle onClick={() => setPopoverKey((prevKey) => prevKey + 1)} size={20} color={getIconColor('primary')} />
</div>
<Link to={Paths.profile} className='flex flex-col gap-2 items-center justify-center border-b border-border pb-4'>
<div className='size-14 rounded-full overflow-hidden'>
<img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
</div>
<div>
{data?.data?.user?.emailAddress}
</div>
<div className='text-muted-foreground'>
{data?.data?.user?.email}
</div>
</Link>
<div className='pb-6 px-6 border-b border-border'>
<SideBarItem
icon={<ProfileCircle color={getIconColor('primary')} size={20} />}
title={t('header.profile')}
isActive
link={Paths.profile}
isWithoutLine
/>
<SideBarItem
icon={<Setting2 color={getIconColor('primary')} size={20} />}
title={t('header.setting')}
isActive
link={Paths.setting}
isWithoutLine
/>
</div>
<div className='px-6 -mt-[2px]'>
<SideBarItem
icon={<Logout color={getIconColor('primary')} size={20} />}
title={t('sidebar.logout')}
isActive
link={'#'}
isWithoutLine
isLogout
/>
</div>
</PopoverPanel>
</Popover>
)
}
</div>
</div>
)
}
export default Header