attachment + download attach + suggestion

This commit is contained in:
hamid zarghami
2025-07-21 16:31:38 +03:30
parent 60f459d33b
commit b31b64a7f9
14 changed files with 879 additions and 331 deletions
+227
View File
@@ -0,0 +1,227 @@
import { FC, useState, useRef, useEffect } from 'react'
import { CloseCircle } from 'iconsax-react'
import { EmailSuggestion } from '@/services/EmailSuggestionsService'
interface EmailInputProps {
toEmails: string[]
onAddEmail: (email: string) => void
onRemoveEmail: (email: string) => void
suggestions: EmailSuggestion[]
onSearchSuggestions?: (query: string) => void
}
const EmailInput: FC<EmailInputProps> = ({
toEmails,
onAddEmail,
onRemoveEmail,
suggestions,
onSearchSuggestions
}) => {
const [to, setTo] = useState('')
const [filteredSuggestions, setFilteredSuggestions] = useState<EmailSuggestion[]>([])
const [showSuggestions, setShowSuggestions] = useState(false)
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1)
const typingTimeoutRef = useRef<number | null>(null)
const searchTimeoutRef = useRef<number | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const isValidEmail = (email: string) => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email)
}
const filterSuggestions = (input: string) => {
if (!input.trim()) {
setFilteredSuggestions([])
setShowSuggestions(false)
return
}
// Always search via API for fresh results
if (onSearchSuggestions) {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current)
}
searchTimeoutRef.current = window.setTimeout(() => {
onSearchSuggestions(input)
}, 300)
}
setSelectedSuggestionIndex(-1)
}
// Update filtered suggestions when API suggestions change
useEffect(() => {
if (suggestions.length > 0) {
const filtered = suggestions
.filter(suggestion =>
!toEmails.includes(suggestion.address)
)
.slice(0, 5)
setFilteredSuggestions(filtered)
setShowSuggestions(filtered.length > 0 && to.trim().length > 0)
} else if (to.trim() === '') {
// Clear suggestions when input is empty
setFilteredSuggestions([])
setShowSuggestions(false)
}
}, [suggestions, to, toEmails])
const selectSuggestion = (suggestion: EmailSuggestion) => {
onAddEmail(suggestion.address)
setTo('')
setShowSuggestions(false)
setFilteredSuggestions([])
setSelectedSuggestionIndex(-1)
inputRef.current?.focus()
}
const addEmail = (email: string) => {
if (isValidEmail(email.trim())) {
onAddEmail(email.trim())
setTo('')
setShowSuggestions(false)
setFilteredSuggestions([])
}
}
const handleToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value
setTo(newValue)
filterSuggestions(newValue)
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
if (newValue.trim()) {
typingTimeoutRef.current = window.setTimeout(() => {
if (isValidEmail(newValue.trim())) {
addEmail(newValue.trim())
}
}, 1500)
}
}
const handleToBlur = () => {
setTimeout(() => {
setShowSuggestions(false)
setFilteredSuggestions([])
}, 200)
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
if (to.trim() && isValidEmail(to.trim())) {
addEmail(to.trim())
}
}
const handleToFocus = () => {
if (to.trim()) {
filterSuggestions(to)
}
}
const handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (showSuggestions && filteredSuggestions.length > 0) {
if (e.key === 'ArrowDown') {
e.preventDefault()
setSelectedSuggestionIndex(prev =>
prev < filteredSuggestions.length - 1 ? prev + 1 : 0
)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setSelectedSuggestionIndex(prev =>
prev > 0 ? prev - 1 : filteredSuggestions.length - 1
)
} else if (e.key === 'Enter' && selectedSuggestionIndex >= 0) {
e.preventDefault()
selectSuggestion(filteredSuggestions[selectedSuggestionIndex])
return
} else if (e.key === 'Escape') {
setShowSuggestions(false)
setFilteredSuggestions([])
setSelectedSuggestionIndex(-1)
return
}
}
if (e.key === 'Enter' && to.trim()) {
e.preventDefault()
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
addEmail(to.trim())
}
}
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current)
}
}
}, [])
return (
<div className='w-full relative'>
<div className="w-full bg-white min-h-10 text-black flex flex-wrap items-center gap-1 px-4 py-2 text-xs rounded-xl border border-border">
{toEmails.map((email, index) => (
<div
key={index}
className="flex items-center gap-1 bg-[#EBEDF5] text-xs h-7 rounded-full px-3"
>
<span>{email}</span>
<CloseCircle
size={16}
color='#D52903'
className='cursor-pointer'
onClick={() => onRemoveEmail(email)}
/>
</div>
))}
<input
ref={inputRef}
value={to}
onChange={handleToChange}
onBlur={handleToBlur}
onFocus={handleToFocus}
onKeyDown={handleToKeyDown}
placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""}
className="flex-1 min-w-32 bg-transparent outline-none"
/>
</div>
{showSuggestions && filteredSuggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 bg-white border border-gray-200 rounded-lg shadow-lg z-[10000] mt-1">
{filteredSuggestions.map((suggestion, index) => (
<div
key={suggestion.address}
className={`px-4 py-2 cursor-pointer text-sm hover:bg-gray-100 border-b border-gray-100 last:border-b-0 ${selectedSuggestionIndex === index ? 'bg-blue-50 text-blue-600' : 'text-gray-700'
}`}
onMouseDown={() => selectSuggestion(suggestion)}
onMouseEnter={() => setSelectedSuggestionIndex(index)}
>
<div className="flex flex-col">
<span className="font-medium">{suggestion.name || suggestion.address}</span>
{suggestion.name && (
<span className="text-xs text-gray-500">{suggestion.address}</span>
)}
</div>
</div>
))}
</div>
)}
</div>
)
}
export default EmailInput