Added alphabetical tabs

X-Lovable-Edit-ID: edt-7760f53e-bc6b-4db9-9ebb-9f5872f6102c
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 04:26:19 +00:00
co-authored by renee-png
+52 -5
View File
@@ -43,6 +43,7 @@ function ContactsIndex() {
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [bulkType, setBulkType] = useState<string>("");
const [letter, setLetter] = useState<string>("all");
const load = async () => {
setLoading(true);
@@ -70,7 +71,12 @@ function ContactsIndex() {
load();
}, []);
const filtered = useMemo(() => {
const letterOf = (name: string | null | undefined) => {
const ch = (name?.[0] ?? "#").toUpperCase();
return /[A-Z]/.test(ch) ? ch : "#";
};
const preLetterFiltered = useMemo(() => {
const term = q.trim().toLowerCase();
return rows.filter((r) => {
if (view === "archived" ? !r.archived_at : !!r.archived_at) return false;
@@ -85,13 +91,26 @@ function ContactsIndex() {
});
}, [rows, q, typeFilter, view]);
const letterCounts = useMemo(() => {
const counts: Record<string, number> = { all: preLetterFiltered.length };
for (const r of preLetterFiltered) {
const l = letterOf(r.name);
counts[l] = (counts[l] ?? 0) + 1;
}
return counts;
}, [preLetterFiltered]);
const filtered = useMemo(() => {
if (letter === "all") return preLetterFiltered;
return preLetterFiltered.filter((r) => letterOf(r.name) === letter);
}, [preLetterFiltered, letter]);
const grouped = useMemo(() => {
const map = new Map<string, ContactRow[]>();
for (const r of filtered) {
const ch = (r.name?.[0] ?? "#").toUpperCase();
const letter = /[A-Z]/.test(ch) ? ch : "#";
if (!map.has(letter)) map.set(letter, []);
map.get(letter)!.push(r);
const l = letterOf(r.name);
if (!map.has(l)) map.set(l, []);
map.get(l)!.push(r);
}
return Array.from(map.entries()).sort(([a], [b]) => {
if (a === "#") return 1;
@@ -265,6 +284,34 @@ function ContactsIndex() {
))}
</div>
<div className="mb-4 flex flex-wrap gap-1">
<Button
size="sm"
variant={letter === "all" ? "default" : "outline"}
className="h-8 px-2.5"
onClick={() => setLetter("all")}
>
All ({letterCounts.all ?? 0})
</Button>
{"ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").concat("#").map((L) => {
const count = letterCounts[L] ?? 0;
const disabled = count === 0;
return (
<Button
key={L}
size="sm"
variant={letter === L ? "default" : "outline"}
className="h-8 w-8 p-0 text-xs"
disabled={disabled}
onClick={() => setLetter(L)}
title={`${L} (${count})`}
>
{L}
</Button>
);
})}
</div>
{selected.size > 0 && (
<Card className="mb-4 border-primary/50 bg-primary/5">
<CardContent className="p-3 flex flex-wrap items-center gap-2">