{/* Desktop sidebar */}
{/* Map + mobile overlays */}
@@ -462,7 +467,7 @@ export function PlacesMap({ places }: PlacesMapProps) {
)}
{/* Full panel: visible when sheet above peek */}
- {snap !== SHEET_SNAP_POINTS[0] && (
+ {snap !== SHEET_SNAP_POINTS[0] && !isDesktop && (
)}
diff --git a/src/components/map/results-list.test.tsx b/src/components/map/results-list.test.tsx
index 105eb0e..50dff45 100644
--- a/src/components/map/results-list.test.tsx
+++ b/src/components/map/results-list.test.tsx
@@ -1,7 +1,7 @@
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { ResultsList } from "@/components/map/results-list";
+import { ResultsList, type ResultRow } from "@/components/map/results-list";
beforeEach(() => {
// The verified badge expires with time, so these tests run on a fixed date.
@@ -81,3 +81,120 @@ describe("ResultsList verified badge", () => {
expect(within(row as HTMLElement).queryByText("Verified")).toBeNull();
});
});
+
+describe("ResultsList paging", () => {
+ const rowsOf = (count: number): ResultRow[] =>
+ Array.from({ length: count }, (_, i) => ({
+ place: {
+ id: `place-${i}`,
+ name: `Place ${i}`,
+ type: "library" as const,
+ city: "mumbai",
+ lat: 19,
+ lng: 72,
+ gmaps_link: "https://maps.google.com/?q=19,72",
+ added_by: "test",
+ },
+ }));
+ const emptyState = { activeFilters: [], onReset: vi.fn(), onSuggest: vi.fn() };
+
+ it("renders one page of rows and a Show more button for the rest", () => {
+ render(
+
,
+ );
+
+ expect(screen.getByText("Place 99")).toBeTruthy();
+ expect(screen.queryByText("Place 100")).toBeNull();
+ expect(
+ screen.getByRole("button", { name: "Show 100 more (150 not shown)" }),
+ ).toBeTruthy();
+ });
+
+ it("reveals the next page and then the remainder", () => {
+ render(
+
,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /^Show 100 more/ }));
+ expect(screen.getByText("Place 199")).toBeTruthy();
+ expect(screen.queryByText("Place 200")).toBeNull();
+
+ fireEvent.click(screen.getByRole("button", { name: "Show 50 more (50 not shown)" }));
+ expect(screen.getByText("Place 249")).toBeTruthy();
+ expect(screen.queryByRole("button", { name: /Show .* more/ })).toBeNull();
+ });
+
+ it("shows no button when everything fits on one page", () => {
+ render(
+
,
+ );
+
+ expect(screen.getAllByRole("listitem")).toHaveLength(100);
+ expect(screen.queryByRole("button", { name: /Show .* more/ })).toBeNull();
+ });
+
+ it("keeps the focused row visible even when it is beyond the first page", () => {
+ render(
+
,
+ );
+
+ const row = screen.getByText("Place 230").closest("li");
+ expect(row).not.toBeNull();
+ expect(
+ screen.getByRole("button", { name: "Show 100 more (149 not shown)" }),
+ ).toBeTruthy();
+ });
+
+ it("starts again from the first page when the result set changes", () => {
+ const first = rowsOf(250);
+ const { rerender } = render(
+
,
+ );
+ fireEvent.click(screen.getByRole("button", { name: /^Show 100 more/ }));
+ expect(screen.getByText("Place 150")).toBeTruthy();
+
+ rerender(
+
,
+ );
+
+ expect(screen.queryByText("Place 150")).toBeNull();
+ expect(screen.getByText("Place 99")).toBeTruthy();
+ });
+});
diff --git a/src/components/map/results-list.tsx b/src/components/map/results-list.tsx
index e8510c6..d5a0ca8 100644
--- a/src/components/map/results-list.tsx
+++ b/src/components/map/results-list.tsx
@@ -1,5 +1,6 @@
"use client";
+import * as React from "react";
import { ExternalLink, MapPinPlus, MapPinX, Navigation, RotateCcw } from "lucide-react";
import type { Place } from "@/lib/types";
@@ -28,8 +29,13 @@ interface ResultsListProps {
focusId?: string | null;
/** Optional expand/collapse control (e.g. "Show all" for the nearest list). */
toggle?: { label: string; onClick: () => void } | null;
+ /** Rows rendered at once; a "Show more" button reveals the next page. */
+ pageSize?: number;
}
+/** Rows rendered before "Show more", so the unfiltered map does not build ~2,000 rows. */
+export const RESULTS_PAGE_SIZE = 100;
+
/** A scrollable list of places. Clicking a row flies the map to that pin. */
export function ResultsList({
header,
@@ -38,7 +44,26 @@ export function ResultsList({
onSelect,
focusId,
toggle,
+ pageSize = RESULTS_PAGE_SIZE,
}: ResultsListProps) {
+ const [limit, setLimit] = React.useState(pageSize);
+
+ // A new result set (filter, search, nearest-first) starts again from the first page.
+ const [seenRows, setSeenRows] = React.useState(rows);
+ if (rows !== seenRows) {
+ setSeenRows(rows);
+ setLimit(pageSize);
+ }
+
+ const shownRows = rows.slice(0, limit);
+ // A focused pin beyond the page still needs its row, or the highlight would vanish.
+ if (focusId && rows.length > limit) {
+ const focused = rows.find((row) => row.place.id === focusId);
+ if (focused && !shownRows.includes(focused)) shownRows.push(focused);
+ }
+ const nextPage = Math.min(pageSize, rows.length - limit);
+ const hiddenCount = rows.length - shownRows.length;
+
return (
@@ -90,7 +115,7 @@ export function ResultsList({
/>
) : (