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
2 changes: 2 additions & 0 deletions SELF-HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
33 changes: 33 additions & 0 deletions src/components/map/map-view.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<MapView places={[]} />);

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"),
);
});
});
50 changes: 49 additions & 1 deletion src/components/map/map-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,35 @@ interface MapViewProps {
closePopupTrigger?: number;
}

let didWarnAboutMissingMapTilerKey = false;

function BasemapUnavailable() {
return (
<div
role="status"
aria-label="Basemap unavailable"
className="flex size-full items-center justify-center bg-muted p-6 text-center"
>
<div className="max-w-sm space-y-2 text-sm text-muted-foreground">
<p className="font-medium text-foreground">Basemap unavailable</p>
<p>
The site administrator needs to configure{" "}
<code className="font-mono text-foreground">
NEXT_PUBLIC_MAPTILER_KEY
</code>{" "}
before the map can load.
</p>
<a
href="/docs/self-hosting"
className="inline-flex font-medium text-primary underline underline-offset-4"
>
Open the self-hosting setup
</a>
</div>
</div>
);
}

/** Closes all open popups whenever the trigger counter increments. */
function ClosePopupOnTrigger({ trigger }: { trigger: number }) {
const map = useMap();
Expand Down Expand Up @@ -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 <BasemapUnavailable />;
}

const focusPlace = focusId
? places.find((place) => place.id === focusId)
: undefined;
Expand Down Expand Up @@ -445,7 +493,7 @@ export default function MapView({
<TileLayer
key={tileVariant}
attribution='&copy; <a href="https://www.maptiler.com/copyright/" target="_blank">MapTiler</a> &copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors'
url={`https://api.maptiler.com/maps/${tileVariant}/256/{z}/{x}/{y}{r}.png?key=${process.env.NEXT_PUBLIC_MAPTILER_KEY}`}
url={`https://api.maptiler.com/maps/${tileVariant}/256/{z}/{x}/{y}{r}.png?key=${mapTilerKey}`}
maxZoom={20}
keepBuffer={2}
updateWhenZooming={false}
Expand Down
Loading