attachment + download attach + suggestion
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user