mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
217 lines
8.8 KiB
TypeScript
217 lines
8.8 KiB
TypeScript
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
|
import { useSearchContext } from "./context"
|
|
import { Button } from "@/components/ui/button"
|
|
import { cn } from "@/lib/utils"
|
|
import { Check, ChevronDown, Mail, X } from "lucide-react"
|
|
import React from "react"
|
|
import { useContacts } from "@/hooks/use-contacts"
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@/components/ui/command"
|
|
|
|
export function MailFilterPopover() {
|
|
const { t } = useTranslation()
|
|
const { filter, setFilter } = useSearchContext()
|
|
const fields = ['from', 'to', 'cc', 'bcc'] as const
|
|
|
|
const activeCount = fields.filter(k => !!filter[k]).length
|
|
|
|
const updateFilter = (field: string, email: string | undefined) => {
|
|
setFilter(prev => ({
|
|
...prev,
|
|
[field]: email
|
|
}))
|
|
}
|
|
|
|
const resetAll = () => {
|
|
setFilter(prev => {
|
|
const next = { ...prev }
|
|
fields.forEach(k => delete next[k])
|
|
return next
|
|
})
|
|
}
|
|
|
|
return (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className={cn(
|
|
'h-8 rounded-none px-3 gap-1.5 transition-colors',
|
|
activeCount > 0 && 'bg-primary/10 text-primary hover:bg-primary/20'
|
|
)}
|
|
>
|
|
<Mail className="h-3.5 w-3.5 opacity-60" />
|
|
<span>
|
|
{activeCount > 0
|
|
? t('search_contacts.label_with_count', { count: activeCount })
|
|
: t('search_contacts.label')}
|
|
</span>
|
|
<ChevronDown className="h-3.5 w-3.5 opacity-60" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
|
|
<PopoverContent
|
|
align="start"
|
|
className="w-fit min-w-[280px] max-w-[90vw] sm:max-w-[min(90vw,500px)] p-0 flex flex-col divide-y divide-border shadow-xl"
|
|
>
|
|
<div className="flex flex-col bg-muted/20 divide-y divide-border">
|
|
{fields.map((field) => (
|
|
<ContactSelectorField
|
|
key={field}
|
|
label={t(`search.${field}`)}
|
|
value={filter[field] as string | undefined}
|
|
onSelect={(email) => updateFilter(field, email)}
|
|
onReset={() => updateFilter(field, undefined)}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{activeCount > 0 && (
|
|
<div className="px-1 pb-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={resetAll}
|
|
className="flex h-8 w-full items-center justify-start gap-2 px-2 text-xs font-medium text-destructive hover:bg-destructive/10 hover:text-destructive transition-colors"
|
|
>
|
|
<div className="flex h-4 w-4 items-center justify-center">
|
|
<X className="h-3.5 w-3.5" />
|
|
</div>
|
|
<span className="flex-1 text-left">
|
|
{t('search_contacts.reset_all')}
|
|
</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|
|
|
|
function ContactSelectorField({
|
|
label,
|
|
value,
|
|
onSelect,
|
|
onReset
|
|
}: {
|
|
label: string
|
|
value?: string
|
|
onSelect: (email: string | undefined) => void
|
|
onReset: () => void
|
|
}) {
|
|
const { t } = useTranslation()
|
|
const [searchTerm, setSearchTerm] = React.useState("")
|
|
const { contacts, isLoading } = useContacts(searchTerm)
|
|
|
|
const handleToggle = (email: string) => {
|
|
if (value === email) {
|
|
onReset()
|
|
} else {
|
|
onSelect(email)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
className={cn(
|
|
"group flex items-center justify-between w-full px-4 py-3 hover:bg-background transition-all text-left relative",
|
|
"min-h-[52px]",
|
|
value && "bg-background/60 hover:bg-background/80"
|
|
)}
|
|
>
|
|
<div className="flex flex-col items-start pr-6">
|
|
<span className="text-[10px] font-bold uppercase opacity-50 tracking-tight leading-none">
|
|
{label}
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
"mt-0.5 truncate max-w-[320px]",
|
|
value
|
|
? "text-xs font-semibold text-primary"
|
|
: "text-xs text-muted-foreground/90"
|
|
)}
|
|
>
|
|
{value || t('search_contacts.any')}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
{value && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onReset()
|
|
}}
|
|
className="p-1 rounded-full hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{value && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
|
|
)}
|
|
</button>
|
|
</PopoverTrigger>
|
|
|
|
<PopoverContent
|
|
side="right"
|
|
align="start"
|
|
className="p-0 w-auto min-w-[300px] max-w-[420px] shadow-2xl border-border/50"
|
|
>
|
|
<Command shouldFilter={false}>
|
|
<CommandInput
|
|
placeholder={t('search_contacts.search_placeholder', { field: label })}
|
|
className="h-9"
|
|
value={searchTerm}
|
|
onValueChange={setSearchTerm}
|
|
/>
|
|
<CommandList className="max-h-[360px]">
|
|
{isLoading && (
|
|
<div className="p-4 text-xs text-center opacity-50">{t('search_contacts.loading')}</div>
|
|
)}
|
|
<CommandEmpty>{t('search_contacts.no_contact_found')}</CommandEmpty>
|
|
<CommandGroup>
|
|
{contacts.slice(0, 100).map((email) => (
|
|
<CommandItem
|
|
key={email}
|
|
onSelect={() => handleToggle(email)}
|
|
className="flex items-center justify-between py-2.5 px-3 cursor-pointer whitespace-nowrap gap-4 text-xs"
|
|
>
|
|
<div className="flex flex-col min-w-0">
|
|
<span className="font-medium">
|
|
{email.split('@')[0]}
|
|
</span>
|
|
<span className="text-[10px] text-muted-foreground truncate max-w-[360px]">
|
|
{email}
|
|
</span>
|
|
</div>
|
|
{value === email && (
|
|
<Check className="h-4 w-4 text-primary shrink-0" />
|
|
)}
|
|
</CommandItem>
|
|
))}
|
|
{contacts.length > 100 && (
|
|
<div className="px-3 py-2 text-[10px] text-center text-muted-foreground border-t border-border/50">
|
|
{t('search_contacts.showing_limit', { total: contacts.length })}
|
|
</div>
|
|
)}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
} |