Added server-side contact search
X-Lovable-Edit-ID: edt-5d552ed5-8362-4deb-98ab-fcb3b9d7185b Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -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<PickResult[]>([]);
|
||||
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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -76,7 +91,9 @@ export function ContactPickerPopover({
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-72 overflow-auto">
|
||||
{filtered.length === 0 ? (
|
||||
{loading ? (
|
||||
<p className="text-xs text-muted-foreground p-3">Searching…</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground p-3">No matching contacts.</p>
|
||||
) : (
|
||||
<ul className="divide-y">
|
||||
|
||||
@@ -172,32 +172,47 @@ function ContactPickerDialog({
|
||||
}) {
|
||||
const [q, setQ] = useState("");
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -210,7 +225,9 @@ function ContactPickerDialog({
|
||||
<Input className="pl-9" placeholder="Search by name, company, email…" value={q} onChange={(e) => setQ(e.target.value)} autoFocus />
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-auto -mx-2">
|
||||
{filtered.length === 0 ? (
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground p-3">Searching…</p>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground p-3">No matching contacts.</p>
|
||||
) : (
|
||||
<ul className="divide-y">
|
||||
|
||||
Reference in New Issue
Block a user