Alphabetized sidebar, capped contacts

X-Lovable-Edit-ID: edt-a4954213-82e2-4a60-86d5-4a1f40df5d2b
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 16:03:07 +00:00
co-authored by renee-png
2 changed files with 34 additions and 18 deletions
+10 -10
View File
@@ -38,23 +38,23 @@ interface NavItem {
}
const NAV: NavItem[] = [
{ to: "/", label: "Dashboard", icon: LayoutDashboard },
{ to: "/admin/users", label: "Users", icon: ShieldCheck, adminOnly: true },
{ to: "/calendar", label: "Calendar", icon: CalendarIcon },
{ to: "/clients", label: "Clients", icon: Users },
{ to: "/contacts", label: "Contacts", icon: UserPlus },
{ to: "/cases", label: "Cases", icon: Briefcase },
{ to: "/tasks", label: "Tasks", icon: CheckSquare },
{ to: "/messages", label: "Messages", icon: MessageSquare },
{ to: "/inbox", label: "Inbox", icon: InboxIcon },
{ to: "/status", label: "Status Updates", icon: Activity },
{ to: "/clients", label: "Clients", icon: Users },
{ to: "/collections", label: "Collections", icon: DollarSign },
{ to: "/documents", label: "Pleadings", icon: FolderOpen },
{ to: "/contacts", label: "Contacts", icon: UserPlus },
{ to: "/", label: "Dashboard", icon: LayoutDashboard },
{ to: "/files", label: "Files", icon: FilesIcon },
{ to: "/forms", label: "Forms", icon: ClipboardList },
{ to: "/inbox", label: "Inbox", icon: InboxIcon },
{ to: "/invoices", label: "Invoices", icon: Receipt },
{ to: "/messages", label: "Messages", icon: MessageSquare },
{ to: "/documents", label: "Pleadings", icon: FolderOpen },
{ to: "/settings", label: "Settings", icon: SettingsIcon },
{ to: "/admin/users", label: "Users", icon: ShieldCheck, adminOnly: true },
];
{ to: "/status", label: "Status Updates", icon: Activity },
{ to: "/tasks", label: "Tasks", icon: CheckSquare },
].sort((a, b) => a.label.localeCompare(b.label));
void FileText;
export function AppShell({ children }: { children: ReactNode }) {
+24 -8
View File
@@ -44,6 +44,7 @@ function ContactsIndex() {
const [selected, setSelected] = useState<Set<string>>(new Set());
const [bulkType, setBulkType] = useState<string>("");
const [letter, setLetter] = useState<string>("all");
const [visibleLimit, setVisibleLimit] = useState<number>(100);
const load = async () => {
setLoading(true);
@@ -140,9 +141,17 @@ function ContactsIndex() {
return preLetterFiltered.filter((r) => letterOf(r.name) === letter);
}, [preLetterFiltered, letter]);
// Reset paging window when filters change
useEffect(() => {
setVisibleLimit(100);
}, [q, typeFilter, view, letter]);
const visibleRows = useMemo(() => filtered.slice(0, visibleLimit), [filtered, visibleLimit]);
const hasMore = filtered.length > visibleRows.length;
const grouped = useMemo(() => {
const map = new Map<string, ContactRow[]>();
for (const r of filtered) {
for (const r of visibleRows) {
const l = letterOf(r.name);
if (!map.has(l)) map.set(l, []);
map.get(l)!.push(r);
@@ -152,7 +161,7 @@ function ContactsIndex() {
if (b === "#") return -1;
return a.localeCompare(b);
});
}, [filtered]);
}, [visibleRows]);
const archivedCount = rows.filter((r) => r.archived_at).length;
const activeCount = rows.length - archivedCount;
@@ -199,12 +208,12 @@ function ContactsIndex() {
});
};
const toggleAllVisible = () => {
const visibleIds = filtered.map((r) => r.id);
const allSelected = visibleIds.every((id) => selected.has(id));
const ids = visibleRows.map((r) => r.id);
const allSelected = ids.every((id) => selected.has(id));
setSelected((s) => {
const next = new Set(s);
if (allSelected) visibleIds.forEach((id) => next.delete(id));
else visibleIds.forEach((id) => next.add(id));
if (allSelected) ids.forEach((id) => next.delete(id));
else ids.forEach((id) => next.add(id));
return next;
});
};
@@ -253,7 +262,7 @@ function ContactsIndex() {
load();
};
const visibleIds = filtered.map((r) => r.id);
const visibleIds = visibleRows.map((r) => r.id);
const allVisibleSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id));
const someVisibleSelected = visibleIds.some((id) => selected.has(id));
@@ -387,7 +396,7 @@ function ContactsIndex() {
aria-label="Select all visible"
/>
<span className="text-xs text-muted-foreground">
Select all {filtered.length} visible
Showing {visibleRows.length} of {filtered.length}
</span>
</div>
)}
@@ -468,6 +477,13 @@ function ContactsIndex() {
))}
</div>
)}
{!loading && hasMore && (
<div className="px-4 py-3 border-t flex items-center justify-center">
<Button variant="outline" size="sm" onClick={() => setVisibleLimit((n) => n + 100)}>
Load 100 more ({filtered.length - visibleRows.length} remaining)
</Button>
</div>
)}
</CardContent>
</Card>