"use client";
import { useState, useMemo } from "react";
import {
Users,
Search,
Check,
X,
Clock,
UserPlus,
Loader2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
type Contact = {
id: string;
name: string;
email: string;
avatar?: string;
initials: string;
};
const allContacts: Contact[] = [
{ id: "1", name: "Alice Johnson", email: "[email protected]", initials: "AJ" },
{ id: "2", name: "Bob Smith", email: "[email protected]", initials: "BS" },
{ id: "3", name: "Carol Williams", email: "[email protected]", initials: "CW" },
{ id: "4", name: "David Brown", email: "[email protected]", initials: "DB" },
{ id: "5", name: "Emma Davis", email: "[email protected]", initials: "ED" },
{ id: "6", name: "Frank Miller", email: "[email protected]", initials: "FM" },
{ id: "7", name: "Grace Wilson", email: "[email protected]", initials: "GW" },
{ id: "8", name: "Henry Taylor", email: "[email protected]", initials: "HT" },
{ id: "9", name: "Ivy Anderson", email: "[email protected]", initials: "IA" },
{ id: "10", name: "Jack Thomas", email: "[email protected]", initials: "JT" },
];
const recentContactIds = ["1", "3", "5"];
export const title = "React Dialog Block Contact Picker";
export default function DialogContactPicker() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [isConfirming, setIsConfirming] = useState(false);
const recentContacts = allContacts.filter((c) => recentContactIds.includes(c.id));
const groupedContacts = useMemo(() => {
const filtered = allContacts.filter(
(c) =>
c.name.toLowerCase().includes(search.toLowerCase()) ||
c.email.toLowerCase().includes(search.toLowerCase())
);
const groups: Record<string, Contact[]> = {};
filtered.forEach((contact) => {
const letter = contact.name[0].toUpperCase();
if (!groups[letter]) groups[letter] = [];
groups[letter].push(contact);
});
return Object.entries(groups).sort(([a], [b]) => a.localeCompare(b));
}, [search]);
const toggleContact = (id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const handleSelectAll = () => {
if (selectedIds.size === allContacts.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(allContacts.map((c) => c.id)));
}
};
const handleConfirm = async () => {
setIsConfirming(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsConfirming(false);
setOpen(false);
};
const handleClose = () => {
setOpen(false);
setSearch("");
};
const selectedContacts = allContacts.filter((c) => selectedIds.has(c.id));
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<Users className="mr-2 h-4 w-4" />
Select Contacts
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-lg p-0 overflow-hidden">
<DialogHeader className="p-4 pb-0">
<DialogTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Select Contacts
</DialogTitle>
<DialogDescription>
Choose one or more contacts to share with.
</DialogDescription>
</DialogHeader>
{/* Selected Preview */}
{selectedIds.size > 0 && (
<div className="px-4 py-2 border-b">
<div className="flex items-center gap-2 flex-wrap">
{selectedContacts.slice(0, 3).map((contact) => (
<Badge
key={contact.id}
variant="secondary"
className="gap-1 pr-1"
>
{contact.name.split(" ")[0]}
<button
onClick={() => toggleContact(contact.id)}
className="ml-1 rounded-full p-0.5 hover:bg-muted"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
{selectedIds.size > 3 && (
<Badge variant="outline">+{selectedIds.size - 3} more</Badge>
)}
<Button
variant="ghost"
size="sm"
className="h-6 text-xs ml-auto"
onClick={() => setSelectedIds(new Set())}
>
Clear all
</Button>
</div>
</div>
)}
<Command className="border-0">
<div className="flex items-center border-b px-3">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandInput
placeholder="Search contacts..."
value={search}
onValueChange={setSearch}
className="border-0 focus:ring-0"
/>
</div>
<CommandList className="max-h-[300px]">
<CommandEmpty>No contacts found.</CommandEmpty>
{/* Recent Contacts */}
{!search && (
<>
<CommandGroup heading="Recent">
{recentContacts.map((contact) => (
<CommandItem
key={`recent-${contact.id}`}
onSelect={() => toggleContact(contact.id)}
className="cursor-pointer"
>
<div className="flex items-center gap-3 flex-1">
<Checkbox
checked={selectedIds.has(contact.id)}
className="pointer-events-none"
/>
<Clock className="h-3 w-3 text-muted-foreground" />
<Avatar className="h-8 w-8">
<AvatarImage src={contact.avatar} />
<AvatarFallback className="text-xs">
{contact.initials}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">
{contact.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{contact.email}
</p>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
</>
)}
{/* Alphabetical Groups */}
{groupedContacts.map(([letter, contacts]) => (
<CommandGroup key={letter} heading={letter}>
{contacts.map((contact) => (
<CommandItem
key={contact.id}
onSelect={() => toggleContact(contact.id)}
className="cursor-pointer"
>
<div className="flex items-center gap-3 flex-1">
<Checkbox
checked={selectedIds.has(contact.id)}
className="pointer-events-none"
/>
<Avatar className="h-8 w-8">
<AvatarImage src={contact.avatar} />
<AvatarFallback className="text-xs">
{contact.initials}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">
{contact.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{contact.email}
</p>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
<DialogFooter className="p-4 border-t">
<div className="flex items-center gap-2 mr-auto">
<Button variant="ghost" size="sm" onClick={handleSelectAll}>
{selectedIds.size === allContacts.length ? "Deselect all" : "Select all"}
</Button>
</div>
<Button variant="outline" onClick={handleClose} disabled={isConfirming}>
Cancel
</Button>
<Button
onClick={handleConfirm}
disabled={selectedIds.size === 0 || isConfirming}
>
{isConfirming ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Adding...
</>
) : (
<>
<Check className="mr-2 h-4 w-4" />
Add {selectedIds.size > 0 ? `(${selectedIds.size})` : ""}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}