389 lines
19 KiB
TypeScript
389 lines
19 KiB
TypeScript
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 |