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
42 changes: 42 additions & 0 deletions src/components/map/filter-presets.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { FilterPresets } from "@/components/map/filter-presets";

afterEach(cleanup);

describe("FilterPresets", () => {
it("lists the preset buttons in a labelled group", () => {
render(<FilterPresets onApply={vi.fn()} />);

const group = screen.getByRole("group", { name: "Filter presets" });
expect(group).toBeTruthy();
for (const label of [
"Libraries",
"SAT centres",
"Exam centres",
"Govt offices",
"Airports",
]) {
expect(screen.getByRole("button", { name: label })).toBeTruthy();
}
});

it("applies one type for a single-type preset", () => {
const onApply = vi.fn();
render(<FilterPresets onApply={onApply} />);

fireEvent.click(screen.getByRole("button", { name: "Airports" }));

expect(onApply).toHaveBeenCalledWith(["airport"]);
});

it("applies both exam types for the Exam centres preset", () => {
const onApply = vi.fn();
render(<FilterPresets onApply={onApply} />);

fireEvent.click(screen.getByRole("button", { name: "Exam centres" }));

expect(onApply).toHaveBeenCalledWith(["sat_centre", "foreign_lang_exam_centre"]);
});
});
3 changes: 3 additions & 0 deletions src/components/map/map-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { CategoryChips } from "@/components/map/category-chips";
import { FilterPresets } from "@/components/map/filter-presets";
import { CityFilter } from "@/components/map/city-filter";
import { NearMeButton } from "@/components/map/near-me-button";
import { MyPlacesPanel } from "@/components/map/my-places-panel";
Expand Down Expand Up @@ -124,6 +125,8 @@ export function MapPanel({
scroll={scrollChips}
/>

<FilterPresets onApply={(types) => onFiltersChange({ ...filters, types })} />

<div className="flex items-center gap-2">
<CityFilter
cities={cities}
Expand Down
15 changes: 15 additions & 0 deletions src/components/map/places-map.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,18 @@ describe("PlacesMap panel mounting", () => {
expect(screen.queryByText("City Library")).toBeNull();
});
});

describe("PlacesMap filter presets", () => {
it("narrows the results to the preset's type and sets it in the URL", () => {
render(<PlacesMap places={PLACES} />);
expect(screen.getByText("City Library")).toBeTruthy();

fireEvent.click(screen.getByRole("button", { name: "Airports" }));

expect(screen.getByText("City Airport")).toBeTruthy();
expect(screen.queryByText("City Library")).toBeNull();
expect(screen.queryByText("Thane Library")).toBeNull();
advance(500);
expect(params().get("types")).toBe("airport");
});
});
56 changes: 56 additions & 0 deletions src/components/pins/pin-popup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { PinPopup } from "@/components/pins/pin-popup";
import type { Place } from "@/lib/types";

vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));

afterEach(cleanup);

const place: Place = {
id: "mum-library-07",
name: "City Library",
type: "library",
city: "mumbai",
lat: 19.0176,
lng: 72.8562,
gmaps_link: "https://maps.google.com/?q=19.0176,72.8562",
added_by: "someone",
};

describe("PinPopup directions", () => {
it("opens the maps-app picker from the directions button", () => {
render(<PinPopup place={place} />);
expect(screen.queryByText("Get directions")).toBeNull();

fireEvent.click(screen.getByRole("button", { name: "Directions to City Library" }));

expect(screen.getByText("Get directions")).toBeTruthy();
const links = screen.getAllByRole("link").map((a) => a.getAttribute("href") ?? "");
expect(links.some((href) => href.startsWith("https://maps.apple.com/"))).toBe(true);
expect(links.some((href) => href.startsWith("geo:19.0176,72.8562"))).toBe(true);
});

it("keeps the direct Google Maps link in the popup", () => {
render(<PinPopup place={place} />);

const google = screen.getByRole("link", { name: /Google Maps/ });
expect(google.getAttribute("href")).toBe(place.gmaps_link);
});
});

describe("PinPopup report a problem", () => {
it("opens the report dialog for this place", () => {
render(<PinPopup place={place} />);
expect(screen.queryByText("Report an issue")).toBeNull();

fireEvent.click(screen.getByRole("button", { name: "Report a problem" }));

expect(screen.getByText("Report an issue")).toBeTruthy();
expect(
screen.getByText(/City Library - opens a pre-filled GitHub issue/),
).toBeTruthy();
expect(screen.getByRole("button", { name: "Open GitHub issue" })).toBeTruthy();
});
});
33 changes: 27 additions & 6 deletions src/components/pins/pin-popup.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"use client";

import * as React from "react";
import { ExternalLink, Link2, Navigation } from "lucide-react";
import { toast } from "sonner";

import type { Place } from "@/lib/types";
import { humanizeCity, PLACE_TYPE_LABELS } from "@/lib/types";
import { directionsUrl, PLACE_TYPE_COLORS } from "@/lib/map";
import { buildShareUrl } from "@/lib/share";
import { DirectionsModal } from "@/components/map/directions-modal";
import { ReportIssueDialog } from "@/components/map/report-issue-dialog";
import { ExamCentreCountdown } from "@/components/pins/exam-centre-countdown";
import { VerifiedBadge } from "@/components/pins/verified-badge";

Expand All @@ -15,6 +18,9 @@ interface PinPopupProps {
}

export function PinPopup({ place }: PinPopupProps) {
const [directionsOpen, setDirectionsOpen] = React.useState(false);
const [reportOpen, setReportOpen] = React.useState(false);

function copyLink() {
const url = buildShareUrl({
types: [],
Expand Down Expand Up @@ -80,15 +86,15 @@ export function PinPopup({ place }: PinPopupProps) {
<ExternalLink className="size-3.5" />
Google Maps
</a>
<a
href={directionsUrl(place.lat, place.lng)}
target="_blank"
rel="noreferrer"
<button
type="button"
onClick={() => setDirectionsOpen(true)}
aria-label={`Directions to ${place.name}`}
aria-haspopup="dialog"
className="inline-flex items-center justify-center rounded-md border border-input bg-background px-2.5 py-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground active:bg-muted/80"
>
<Navigation className="size-3.5" />
</a>
</button>
<button
type="button"
onClick={copyLink}
Expand All @@ -100,12 +106,27 @@ export function PinPopup({ place }: PinPopupProps) {
</div>

{/* Footer badge */}
<div className="border-t border-border/30 pt-2">
<div className="flex items-center justify-between gap-2 border-t border-border/30 pt-2">
<p className="text-[11px] text-muted-foreground/75">
Added by{" "}
<span className="font-medium text-muted-foreground">{place.added_by}</span>
</p>
<button
type="button"
onClick={() => setReportOpen(true)}
aria-haspopup="dialog"
className="text-[11px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
>
Report a problem
</button>
</div>

<DirectionsModal
place={place}
open={directionsOpen}
onOpenChange={setDirectionsOpen}
/>
<ReportIssueDialog place={place} open={reportOpen} onOpenChange={setReportOpen} />
</div>
);
}
Loading