From 1beaa2a2a816112744abef525324eabb97debb1f Mon Sep 17 00:00:00 2001
From: Mikhail Chandran
Date: Thu, 29 Oct 2026 23:33:58 +0530
Subject: [PATCH] Show per-type place counts with map links on city pages
---
src/app/city/[slug]/page.tsx | 4 ++
src/components/home/city-stats-card.test.tsx | 56 ++++++++++++++++++++
2 files changed, 60 insertions(+)
create mode 100644 src/components/home/city-stats-card.test.tsx
diff --git a/src/app/city/[slug]/page.tsx b/src/app/city/[slug]/page.tsx
index 7f1a14b..ed4fab5 100644
--- a/src/app/city/[slug]/page.tsx
+++ b/src/app/city/[slug]/page.tsx
@@ -14,6 +14,7 @@ import { humanizeCity, PLACE_TYPE_LABELS } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { PageContainer } from "@/components/layout/page-container";
+import { CityStatsCard } from "@/components/home/city-stats-card";
// Every city page is decided at build time from the dataset; a slug that is
// not in it is a 404, never a server-rendered miss.
@@ -68,6 +69,9 @@ export default async function CityPage({ params }: CityPageProps) {
the crowdsourced StudyMap dataset. Open one on the map to see it in context.
+ {/* Per-type counts, each linking to /map filtered to this city and type. */}
+
+
{page.places.length <= THIN_CITY_MAX_PLACES && (
diff --git a/src/components/home/city-stats-card.test.tsx b/src/components/home/city-stats-card.test.tsx
new file mode 100644
index 0000000..45967c1
--- /dev/null
+++ b/src/components/home/city-stats-card.test.tsx
@@ -0,0 +1,56 @@
+import { cleanup, render, screen, within } from "@testing-library/react";
+import { afterEach, describe, expect, it } from "vitest";
+
+import { CityStatsCard } from "@/components/home/city-stats-card";
+import type { Place } from "@/lib/types";
+
+afterEach(cleanup);
+
+function place(overrides: Partial & Pick): Place {
+ return {
+ name: overrides.id,
+ city: "navi_mumbai",
+ lat: 19.03,
+ lng: 73.03,
+ gmaps_link: "https://maps.google.com/?q=19.03,73.03",
+ added_by: "test",
+ ...overrides,
+ };
+}
+
+const PLACES: Place[] = [
+ place({ id: "navi-library-01", type: "library" }),
+ place({ id: "navi-library-02", type: "library" }),
+ place({ id: "navi-sat-01", type: "sat_centre" }),
+ place({ id: "mumbai-airport-01", type: "airport", city: "mumbai" }),
+];
+
+describe("CityStatsCard", () => {
+ it("titles the card with the city and counts only that city's places by type", () => {
+ render();
+
+ expect(screen.getByText("Navi Mumbai")).toBeTruthy();
+ const library = screen.getByRole("link", { name: /Library/ });
+ expect(within(library).getByText("2")).toBeTruthy();
+ const sat = screen.getByRole("link", { name: /SAT centre/ });
+ expect(within(sat).getByText("1")).toBeTruthy();
+ expect(screen.queryByRole("link", { name: /Airport/ })).toBeNull();
+ });
+
+ it("links each count to the map filtered to that city and type", () => {
+ render();
+
+ const href = screen.getByRole("link", { name: /Library/ }).getAttribute("href") ?? "";
+ expect(href.startsWith("/map?")).toBe(true);
+ const params = new URLSearchParams(href.slice("/map?".length));
+ expect(params.get("city")).toBe("navi_mumbai");
+ expect(params.get("types")).toBe("library");
+ });
+
+ it("says so when the city has no places", () => {
+ render();
+
+ expect(screen.getByText("No places recorded yet.")).toBeTruthy();
+ expect(screen.queryAllByRole("link")).toHaveLength(0);
+ });
+});