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:
@@ -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 }) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user