Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/app/embed/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";

import { getPlaces } from "@/lib/places";
import { EmbedMap } from "@/components/map/embed-map";
import studyMapConfig from "../../../studymap.config";

export const metadata: Metadata = {
title: "Embedded Map",
Expand All @@ -13,7 +14,11 @@ export default function EmbedPage() {

return (
<div className="h-dvh w-full">
<EmbedMap places={places} />
<EmbedMap
places={places}
defaultCenter={studyMapConfig.center}
defaultZoom={studyMapConfig.defaultZoom}
/>
</div>
);
}
10 changes: 8 additions & 2 deletions src/app/map/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Metadata } from "next";

import { getPlaces } from "@/lib/places";
import { getCities, getPlaces } from "@/lib/places";
import { PlacesMap } from "@/components/map/places-map";
import studyMapConfig from "../../../studymap.config";

export const metadata: Metadata = {
title: "Map",
Expand All @@ -14,7 +15,12 @@ export default function MapPage() {

return (
<div className="h-[calc(100dvh-3.5rem)] w-full">
<PlacesMap places={places} />
<PlacesMap
places={places}
cities={getCities(places)}
defaultCenter={studyMapConfig.center}
defaultZoom={studyMapConfig.defaultZoom}
/>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/home/city-stats-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from "next/link";

import type { Place, PlaceType } from "@/lib/types";
import { PLACE_TYPES, PLACE_TYPE_LABELS, humanizeCity } from "@/lib/types";
import { filterPlaces } from "@/lib/places";
import { filterPlaces } from "@/lib/places-utils";
import { mapStateToSearch } from "@/lib/share";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

Expand Down
13 changes: 8 additions & 5 deletions src/components/map/embed-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import dynamic from "next/dynamic";
import Link from "next/link";

import type { Place } from "@/lib/types";
import { cityBounds, filterPlaces } from "@/lib/places";
import { cityBounds, filterPlaces } from "@/lib/places-utils";
import { parseMapState } from "@/lib/share";
import { site } from "@/lib/site";
import studyMapConfig from "../../../studymap.config";

const MapView = dynamic(() => import("@/components/map/map-view"), {
ssr: false,
Expand All @@ -17,6 +16,8 @@ const MapView = dynamic(() => import("@/components/map/map-view"), {

interface EmbedMapProps {
places: Place[];
defaultCenter: [number, number];
defaultZoom: number;
}

/**
Expand All @@ -26,7 +27,7 @@ interface EmbedMapProps {
* the full map's "Share" button already works as an embed URL. Public
* places only - private/saved places never enter this component.
*/
export function EmbedMap({ places }: EmbedMapProps) {
export function EmbedMap({ places, defaultCenter, defaultZoom }: EmbedMapProps) {
const state = React.useMemo(
() => parseMapState(typeof window === "undefined" ? "" : window.location.search),
[],
Expand Down Expand Up @@ -55,9 +56,11 @@ export function EmbedMap({ places }: EmbedMapProps) {
focusBounds={focusBounds}
closePopupTrigger={0}
center={
state.lat !== null && state.lng !== null ? [state.lat, state.lng] : undefined
state.lat !== null && state.lng !== null
? [state.lat, state.lng]
: defaultCenter
}
zoom={state.zoom ?? studyMapConfig.defaultZoom}
zoom={state.zoom ?? defaultZoom}
/>
</div>
<Link
Expand Down
6 changes: 5 additions & 1 deletion src/components/map/map-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,11 @@ export function MapPanel({
toggle={resultsToggle}
/>

<SuggestPlaceDialog open={suggestOpen} onOpenChange={setSuggestOpen} />
<SuggestPlaceDialog
datasetCities={cities}
open={suggestOpen}
onOpenChange={setSuggestOpen}
/>
<EmbedWidgetDialog open={embedOpen} onOpenChange={setEmbedOpen} />
</div>
);
Expand Down
7 changes: 5 additions & 2 deletions src/components/map/map-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ afterEach(() => {
vi.restoreAllMocks();
});

const VIEW = { center: [19.08, 72.95] as [number, number], zoom: 11 };

describe("MapView configuration", () => {
it("shows actionable guidance when the MapTiler key is blank", () => {
vi.stubEnv("NEXT_PUBLIC_MAPTILER_KEY", " ");
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);

render(<MapView places={[]} />);
render(<MapView places={[]} {...VIEW} />);

expect(screen.getByRole("status", { name: "Basemap unavailable" })).toBeTruthy();
expect(screen.getByText("NEXT_PUBLIC_MAPTILER_KEY")).toBeTruthy();
Expand All @@ -72,6 +74,7 @@ describe("MapView configuration", () => {
);
render(
<MapView
{...VIEW}
places={[
{
id: "heathrow",
Expand Down Expand Up @@ -109,7 +112,7 @@ describe("MapView configuration", () => {
}));
flyTo.mockClear();

render(<MapView places={[]} userLocation={{ lat: 19.2, lng: 72.9 }} />);
render(<MapView places={[]} {...VIEW} userLocation={{ lat: 19.2, lng: 72.9 }} />);

expect(flyTo).toHaveBeenCalledWith(
[19.2, 72.9],
Expand Down
15 changes: 7 additions & 8 deletions src/components/map/map-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import {
import type { Place, PlaceType } from "@/lib/types";
import { PLACE_TYPES, PLACE_TYPE_LABELS } from "@/lib/types";
import type { LatLng } from "@/lib/geo";
import studyMapConfig from "../../../studymap.config";
import type { Bounds } from "@/lib/places";
import type { Bounds } from "@/lib/places-utils";
import { PLACE_TYPE_COLORS } from "@/lib/map";
import { PinPopup } from "@/components/pins/pin-popup";

Expand Down Expand Up @@ -244,10 +243,10 @@ interface MapViewProps {
focusBounds?: Bounds | null;
/** When false, the map is a static preview: no pan, zoom, or controls. */
interactive?: boolean;
/** Initial zoom level; falls back to the MMR default. */
zoom?: number;
/** Initial center, as [lat, lng]; falls back to the configured region center. */
center?: [number, number];
/** Initial zoom level. Callers resolve it from `studymap.config.ts`, so this bundle stays free of the dataset. */
zoom: number;
/** Initial center, as [lat, lng]. */
center: [number, number];
/** When set, the map starts at this viewport instead of fitting all places. */
initialViewport?: MapViewport | null;
/** Fired on pan/zoom end with the current center and zoom. */
Expand Down Expand Up @@ -474,8 +473,8 @@ export default function MapView({
focusId,
focusBounds,
interactive = true,
zoom = studyMapConfig.defaultZoom,
center = studyMapConfig.center,
zoom,
center,
initialViewport = null,
onViewportChange,
closePopupTrigger = 0,
Expand Down
74 changes: 66 additions & 8 deletions src/components/map/places-map.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ function place(overrides: Partial<Place> & Pick<Place, "id" | "name">): Place {
};
}

const CITIES = ["mumbai", "thane"];

const PLACES: Place[] = [
place({ id: "city-library", name: "City Library" }),
place({ id: "reading-room", name: "Riverside Reading Room" }),
Expand Down Expand Up @@ -106,7 +108,14 @@ afterEach(() => {
describe("PlacesMap search URLs", () => {
it("keeps an initial query and the other shared state after mounting", () => {
window.history.replaceState(null, "", FILTERED_URL);
render(<PlacesMap places={PLACES} />);
render(
<PlacesMap
places={PLACES}
cities={CITIES}
defaultCenter={[19.08, 72.95]}
defaultZoom={11}
/>,
);

expect(searchInputs().map((input) => input.value)).toEqual(["Library", "Library"]);
expect(screen.getByText("City Library")).toBeTruthy();
Expand All @@ -129,7 +138,14 @@ describe("PlacesMap search URLs", () => {
});

it("updates results and the URL only after the latest input has settled", () => {
render(<PlacesMap places={PLACES} />);
render(
<PlacesMap
places={PLACES}
cities={CITIES}
defaultCenter={[19.08, 72.95]}
defaultZoom={11}
/>,
);
const replace = vi.spyOn(window.history, "replaceState");
const historyLength = window.history.length;
const [desktop, mobile] = searchInputs();
Expand Down Expand Up @@ -160,7 +176,14 @@ describe("PlacesMap search URLs", () => {

it("shares the applied query and restores the same results from the copied link", async () => {
window.history.replaceState(null, "", FILTERED_URL);
const view = render(<PlacesMap places={PLACES} />);
const view = render(
<PlacesMap
places={PLACES}
cities={CITIES}
defaultCenter={[19.08, 72.95]}
defaultZoom={11}
/>,
);
fireEvent.change(searchInputs()[0], { target: { value: "Reading" } });

await act(async () => {
Expand All @@ -182,7 +205,14 @@ describe("PlacesMap search URLs", () => {
expect(copied.pathname).toBe("/map");
view.unmount();
window.history.replaceState(null, "", copied.href);
render(<PlacesMap places={PLACES} />);
render(
<PlacesMap
places={PLACES}
cities={CITIES}
defaultCenter={[19.08, 72.95]}
defaultZoom={11}
/>,
);

expect(searchInputs().map((input) => input.value)).toEqual(["Reading", "Reading"]);
expect(screen.getByText("Riverside Reading Room")).toBeTruthy();
Expand All @@ -193,7 +223,14 @@ describe("PlacesMap search URLs", () => {

it("clears the query without waiting 250 ms or losing the other shared state", () => {
window.history.replaceState(null, "", FILTERED_URL);
render(<PlacesMap places={PLACES} />);
render(
<PlacesMap
places={PLACES}
cities={CITIES}
defaultCenter={[19.08, 72.95]}
defaultZoom={11}
/>,
);

fireEvent.change(searchInputs()[0], { target: { value: "" } });
advance(0);
Expand All @@ -209,7 +246,14 @@ describe("PlacesMap search URLs", () => {

it("resets the search and filters to an unfiltered URL", () => {
window.history.replaceState(null, "", "/map?q=Library&types=library&city=mumbai");
render(<PlacesMap places={PLACES} />);
render(
<PlacesMap
places={PLACES}
cities={CITIES}
defaultCenter={[19.08, 72.95]}
defaultZoom={11}
/>,
);

fireEvent.click(screen.getByRole("button", { name: "Reset" }));
advance(0);
Expand All @@ -224,7 +268,14 @@ describe("PlacesMap search URLs", () => {

describe("PlacesMap panel mounting", () => {
it("mounts the sidebar panel at desktop width", () => {
render(<PlacesMap places={PLACES} />);
render(
<PlacesMap
places={PLACES}
cities={CITIES}
defaultCenter={[19.08, 72.95]}
defaultZoom={11}
/>,
);

// Sidebar search + mobile top bar search.
expect(searchInputs()).toHaveLength(2);
Expand All @@ -233,7 +284,14 @@ describe("PlacesMap panel mounting", () => {

it("does not build the sidebar panel on a phone", () => {
stubViewport(false);
render(<PlacesMap places={PLACES} />);
render(
<PlacesMap
places={PLACES}
cities={CITIES}
defaultCenter={[19.08, 72.95]}
defaultZoom={11}
/>,
);

// Only the mobile top bar search; the sheet is closed at peek height.
expect(searchInputs()).toHaveLength(1);
Expand Down
Loading
Loading