diff --git a/src/components/clients/contact-picker-popover.tsx b/src/components/clients/contact-picker-popover.tsx index 9717edc..e3072d3 100644 --- a/src/components/clients/contact-picker-popover.tsx +++ b/src/components/clients/contact-picker-popover.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -30,29 +30,44 @@ export function ContactPickerPopover({ const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); useEffect(() => { if (!open) return; setQ(""); - supabase - .from("contacts") - .select("id, name, email, phone, title, company, contact_type") - .is("archived_at", null) - .order("name") - .limit(200) - .then(({ data }) => setRows((data ?? []) as PickResult[])); }, [open]); - const filtered = useMemo(() => { - const term = q.trim().toLowerCase(); - if (!term) return rows; - return rows.filter( - (r) => - r.name.toLowerCase().includes(term) || - (r.company ?? "").toLowerCase().includes(term) || - (r.email ?? "").toLowerCase().includes(term), - ); - }, [rows, q]); + useEffect(() => { + if (!open) return; + let cancelled = false; + const term = q.trim(); + setLoading(true); + const handle = setTimeout(async () => { + let query = supabase + .from("contacts") + .select("id, name, email, phone, title, company, contact_type") + .is("archived_at", null) + .order("name") + .limit(50); + if (term) { + const esc = term.replace(/[%,]/g, " "); + query = query.or( + `name.ilike.%${esc}%,company.ilike.%${esc}%,email.ilike.%${esc}%`, + ); + } + const { data } = await query; + if (!cancelled) { + setRows((data ?? []) as PickResult[]); + setLoading(false); + } + }, 200); + return () => { + cancelled = true; + clearTimeout(handle); + }; + }, [open, q]); + + const filtered = rows; return ( @@ -76,7 +91,9 @@ export function ContactPickerPopover({
- {filtered.length === 0 ? ( + {loading ? ( +

Searching…

+ ) : filtered.length === 0 ? (

No matching contacts.

) : (
    diff --git a/src/components/contacts/contacts-link-tab.tsx b/src/components/contacts/contacts-link-tab.tsx index 9de612c..44f5eec 100644 --- a/src/components/contacts/contacts-link-tab.tsx +++ b/src/components/contacts/contacts-link-tab.tsx @@ -172,32 +172,47 @@ function ContactPickerDialog({ }) { const [q, setQ] = useState(""); const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); useEffect(() => { if (!open) return; setQ(""); - supabase - .from("contacts") - .select("id, name, company, contact_type, email") - .is("archived_at", null) - .order("name") - .limit(200) - .then(({ data }) => setRows(data ?? [])); }, [open]); - const filtered = useMemo(() => { - const term = q.trim().toLowerCase(); - return rows - .filter((r) => !excludeIds.has(r.id)) - .filter((r) => { - if (!term) return true; - return ( - r.name.toLowerCase().includes(term) || - (r.company ?? "").toLowerCase().includes(term) || - (r.email ?? "").toLowerCase().includes(term) + useEffect(() => { + if (!open) return; + let cancelled = false; + const term = q.trim(); + setLoading(true); + const handle = setTimeout(async () => { + let query = supabase + .from("contacts") + .select("id, name, company, contact_type, email") + .is("archived_at", null) + .order("name") + .limit(50); + if (term) { + const esc = term.replace(/[%,]/g, " "); + query = query.or( + `name.ilike.%${esc}%,company.ilike.%${esc}%,email.ilike.%${esc}%`, ); - }); - }, [rows, q, excludeIds]); + } + const { data } = await query; + if (!cancelled) { + setRows(data ?? []); + setLoading(false); + } + }, 200); + return () => { + cancelled = true; + clearTimeout(handle); + }; + }, [open, q]); + + const filtered = useMemo( + () => rows.filter((r) => !excludeIds.has(r.id)), + [rows, excludeIds], + ); return ( @@ -210,7 +225,9 @@ function ContactPickerDialog({ setQ(e.target.value)} autoFocus />
- {filtered.length === 0 ? ( + {loading ? ( +

Searching…

+ ) : filtered.length === 0 ? (

No matching contacts.

) : (