diff --git a/SELF-HOSTING.md b/SELF-HOSTING.md index 228eb97..040f09a 100644 --- a/SELF-HOSTING.md +++ b/SELF-HOSTING.md @@ -56,6 +56,8 @@ cp .env.example .env.local - `NEXT_PUBLIC_MAPTILER_KEY` is required for the map basemap. Free tier, no credit card, at [cloud.maptiler.com](https://cloud.maptiler.com/account/keys/). +- If the key is missing or empty, the map shows "Basemap unavailable" instead of loading tiles. + Add the key to `.env.local`, then restart the development server or redeploy the site. - The Supabase variables are optional. Leave them blank and the map, filters, and calendar all work; you just won't get sign-in or the two private features below. See step 5 to fill them in. diff --git a/src/components/map/map-view.test.tsx b/src/components/map/map-view.test.tsx new file mode 100644 index 0000000..ebabe4f --- /dev/null +++ b/src/components/map/map-view.test.tsx @@ -0,0 +1,33 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import MapView from "@/components/map/map-view"; + +afterEach(() => { + cleanup(); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); +}); + +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(); + + expect( + screen.getByRole("status", { name: "Basemap unavailable" }), + ).toBeTruthy(); + expect(screen.getByText("NEXT_PUBLIC_MAPTILER_KEY")).toBeTruthy(); + expect( + screen + .getByRole("link", { name: "Open the self-hosting setup" }) + .getAttribute("href"), + ).toBe("/docs/self-hosting"); + expect(warn).toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("NEXT_PUBLIC_MAPTILER_KEY is missing"), + ); + }); +}); diff --git a/src/components/map/map-view.tsx b/src/components/map/map-view.tsx index f823d2a..6216cf5 100644 --- a/src/components/map/map-view.tsx +++ b/src/components/map/map-view.tsx @@ -217,6 +217,35 @@ interface MapViewProps { closePopupTrigger?: number; } +let didWarnAboutMissingMapTilerKey = false; + +function BasemapUnavailable() { + return ( +
+
+

Basemap unavailable

+

+ The site administrator needs to configure{" "} + + NEXT_PUBLIC_MAPTILER_KEY + {" "} + before the map can load. +

+ + Open the self-hosting setup + +
+
+ ); +} + /** Closes all open popups whenever the trigger counter increments. */ function ClosePopupOnTrigger({ trigger }: { trigger: number }) { const map = useMap(); @@ -409,6 +438,25 @@ export default function MapView({ onViewportChange, closePopupTrigger = 0, }: MapViewProps) { + const mapTilerKey = process.env.NEXT_PUBLIC_MAPTILER_KEY?.trim(); + + useEffect(() => { + if ( + process.env.NODE_ENV !== "production" && + !mapTilerKey && + !didWarnAboutMissingMapTilerKey + ) { + didWarnAboutMissingMapTilerKey = true; + console.warn( + "[MapView] NEXT_PUBLIC_MAPTILER_KEY is missing. The basemap cannot load. See SELF-HOSTING.md for setup instructions.", + ); + } + }, [mapTilerKey]); + + if (!mapTilerKey) { + return ; + } + const focusPlace = focusId ? places.find((place) => place.id === focusId) : undefined; @@ -445,7 +493,7 @@ export default function MapView({