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
76 changes: 61 additions & 15 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Architecture

StudyMap is a Next.js 16 (App Router) static site. There is no database, no auth, and no server-side logic. All place data ships as JSON in the repo. This document is a map for new contributors.
StudyMap is a Next.js 16 (App Router) app. The public map, calendar, and place data need no
database and no auth - all place data ships as JSON, imported via `studymap.config.ts` at the
repo root. Signing in (Supabase Auth) is optional and only gates two private features: saved
custom places and personal calendar events, both backed by RLS-scoped Supabase tables under
`supabase/migrations/`. This document is a map for new contributors.

---

Expand All @@ -9,15 +13,19 @@ StudyMap is a Next.js 16 (App Router) static site. There is no database, no auth
```
studymap/
├── .github/
│ ├── DISCUSSION_TEMPLATE/
│ │ └── request-a-city.yml # discussion template: request coverage for a new city
│ ├── ISSUE_TEMPLATE/
│ │ ├── add-place.yml # issue form: suggest a new place
│ │ ├── bug_report.yml # issue form: file a bug
│ │ ├── feature_request.yml # issue form: request a feature
│ │ └── config.yml # disables blank issues, adds contact links
│ ├── workflows/ # CI: lint+typecheck, build+validate-data
│ └── pull_request_template.md
├── data/
│ ├── CONTRIBUTING.md # data-specific contribution rules
│ └── places/ # 8 JSON files, one per place type
│ ├── places/ # 9 JSON files, one per place type
│ └── places.sample/ # minimal placeholder dataset for forks (see SELF-HOSTING.md)
├── public/
│ ├── brand/ # OG image (og.svg, og-preview.html)
│ ├── icons/ # PWA icons: 192px + 512px, normal + maskable
Expand All @@ -31,24 +39,34 @@ studymap/
│ ├── app/ # Next.js App Router pages and layouts
│ ├── components/ # React components, grouped by feature
│ └── lib/ # pure TypeScript utilities (no JSX)
├── supabase/
│ └── migrations/ # SQL for the two optional, sign-in-gated tables (user_places,
│ # user_home, user_events), run once by hand - see SELF-HOSTING.md
├── ARCHITECTURE.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── SECURITY.md
├── SELF-HOSTING.md
├── components.json # shadcn/ui component config
├── eslint.config.mjs
├── next.config.js
├── package.json
├── postcss.config.mjs
└── tsconfig.json
├── studymap.config.ts # region + dataset config; the file a fork edits to retarget StudyMap
├── studymap.config.example.ts # starter version of the above, pointing at data/places.sample/
├── tsconfig.json
└── vitest.config.ts
```

### `data/places/`

Eight JSON files, one per place category. Each file is a flat array of place objects. This is the only place place data lives — nothing is duplicated elsewhere.
Nine JSON files, one per place category (imported by `studymap.config.ts` at the repo root, not
directly by `src/lib/places.ts` - see [Data flow](#data-flow) below). Each file is a flat array of
place objects. This is the only place place data lives — nothing is duplicated elsewhere.

| File | Type key |
|------|----------|
Expand All @@ -60,6 +78,7 @@ Eight JSON files, one per place category. Each file is a flat array of place obj
| `stationery.json` | `stationery` |
| `internet_cafe.json` | `internet_cafe` |
| `imp_locations.json` | `imp_locations` |
| `repair_shop.json` | `repair_shop` |

Record shape (defined in `src/lib/types.ts`):

Expand All @@ -68,7 +87,7 @@ Record shape (defined in `src/lib/types.ts`):
id: string; // kebab-case, unique across all types
name: string;
type: PlaceType;
city: "mumbai" | "thane" | "navi_mumbai";
city: string; // free-form slug, e.g. "mumbai", "navi_mumbai" - any city worldwide
lat: number;
lng: number;
address?: string;
Expand All @@ -94,6 +113,8 @@ Next.js App Router. Each folder is a route segment.
| `/about` | `about/page.tsx` | Principles, stats, maintainers |
| `/legal/*` | | Privacy, terms, disclaimer |
| `/offline` | `offline/page.tsx` | PWA offline fallback |
| `/login` | `login/page.tsx` | Optional sign-in (Google OAuth, email) |
| `/auth/callback` | `auth/callback/route.ts` | OAuth code exchange (dynamic, not static-export-safe) |

`layout.tsx` (root) wraps every page with `Navbar`, `Footer`, and the theme provider.

Expand All @@ -108,14 +129,26 @@ components/
│ ├── hero-particles.tsx # canvas particle animation
│ └── map-preview.tsx # static map thumbnail on homepage
├── map/
│ ├── places-map.tsx # top-level map container (filtering, URL state, geolocation)
│ ├── map-view.tsx # Leaflet map (markers, tiles, scroll guard, user dot)
│ ├── filter-panel.tsx # city + type checkboxes, result count, reset
│ └── near-me-button.tsx # triggers browser geolocation
│ ├── places-map.tsx # top-level map container: filtering, URL state, geolocation,
│ │ # auth + saved-places/home state, merges the private pin layer
│ ├── map-view.tsx # Leaflet map (clustering, tiles, scroll guard, user dot)
│ ├── map-panel.tsx # search, category chips, city select, results list, my-places slot
│ ├── my-places-panel.tsx # signed-in-only saved-places section: own search/city filter, CRUD
│ ├── user-place-dialog.tsx # add/edit/delete a saved place
│ ├── user-home-dialog.tsx # set/edit/remove the saved home location
│ ├── category-chips.tsx # type filter chips with counts
│ ├── results-list.tsx # scrollable place list, optionally distance-sorted
│ ├── map-sheet.tsx # mobile bottom sheet (vaul) wrapping MapPanel
│ ├── near-me-button.tsx # desktop geolocation trigger
│ ├── near-me-fab.tsx # mobile floating geolocation trigger
│ ├── map-error-boundary.tsx
│ └── filters.ts # `PlaceFilters` type
├── calendar/
│ └── personal-event-dialog.tsx # add/edit/delete a signed-in user's calendar event
├── pins/
│ └── pin-popup.tsx # popup on marker click (directions, share, added_by)
├── layout/
│ ├── navbar.tsx
│ ├── navbar.tsx # nav links + sign-in/sign-out
│ ├── footer.tsx
│ ├── page-container.tsx # max-width wrapper used by most pages
│ ├── theme-provider.tsx
Expand All @@ -130,6 +163,9 @@ components/
└── pwa-register.tsx # service worker registration for offline / PWA support
```

`src/app/calendar/CalendarView.tsx` (a page-level component, not under `components/`) renders the
public exam calendar and, when signed in, the personal-events overlay.

### `src/lib/`

Pure TypeScript modules. No JSX, no React imports. Each file has a single responsibility.
Expand All @@ -144,7 +180,14 @@ Pure TypeScript modules. No JSX, no React imports. Each file has a single respon
| `site.ts` | Site name, tagline, repo URL, nav link list |
| `exam-dates.ts` | `EXAM_EVENTS` array with SAT/IB/IGCSE session data |
| `fonts.ts` | next/font setup (Inter, Space Grotesk, JetBrains Mono) |
| `utils.ts` | `cn()` — clsx + tailwind-merge helper |
| `utils.ts` | `cn()` — clsx + tailwind-merge helper; `isMissingTableError()` — detects an unrun Supabase migration |
| `user-places.ts` | CRUD for saved places + home location (`user_places`, `user_home` tables) |
| `user-events.ts` | CRUD for personal calendar events (`user_events` table) |
| `supabase/client.ts`, `supabase/server.ts` | Browser and server Supabase clients (`@supabase/ssr`) |

`types.ts`, `places.ts`, `geo.ts`, `map.ts`, `share.ts`, `exam-dates.ts` have no Supabase dependency
and no side effects, they're the easiest targets for new unit tests (see `*.test.ts` next to
`geo.ts`, `share.ts`, and `places.ts`, run via `npm run test:unit`).

---

Expand All @@ -160,14 +203,15 @@ src/lib/places.ts getPlaces() reads the config's Place[]
src/components/map/places-map.tsx
│ reads URL params via share.ts → parseMapState()
│ calls filterPlaces() on every filter change
│ passes filtered Place[] down to MapView
│ if signed in: fetches saved places/home (user-places.ts) and merges
│ them into the Place[] passed down, on top of the public filters
├──▶ src/components/map/filter-panel.tsx (type + city checkboxes)
├──▶ src/components/map/map-panel.tsx (search, chips, city select, my-places-panel)
├──▶ src/components/map/near-me-button.tsx (geolocation → placesByDistance())
src/components/map/map-view.tsx
renders one Leaflet CircleMarker per place, coloured by PLACE_TYPE_COLORS
clusters overlapping pins (Supercluster), coloured by PLACE_TYPE_COLORS
│ on marker click: opens PinPopup
Expand All @@ -176,7 +220,9 @@ src/components/pins/pin-popup.tsx
buildShareUrl(state) ← src/lib/share.ts
```

**No network requests at runtime.** The JSON is bundled at build time via static `import` statements in `studymap.config.ts`. The Leaflet tile layer is the only external request when the map is open.
**The public map makes no network requests at runtime beyond map tiles.** Place JSON is bundled
at build time via static `import` statements in `studymap.config.ts`. Signing in adds real runtime
requests: Supabase Auth, plus `user-places.ts`/`user-events.ts` calls for the two private features.

---

Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Repair shop as a ninth place category (color: red `#DC2626`), wired through the full design system (type definitions, color tokens, CSS custom properties, Tailwind utilities, hero legend).
- Map auto-fits to all loaded places on first load instead of a fixed Mumbai crop.
- CARTO basemap saturation pass for improved visual clarity before the MapTiler migration.
- Config-driven region and dataset: `studymap.config.ts` at the repo root holds `center`, `defaultZoom`, `bounds`, and a `cities` display-order registry, plus the place-data imports. Retargeting StudyMap to a new city or dataset means editing this one file.
- Data-source abstraction: `src/lib/places.ts` now reads places from `studymap.config.ts` instead of importing `data/places/*.json` directly. `src/lib/constants.ts` removed, fully superseded.
- Template-repo support for forks: "Use this template" enabled on the repo, plus `studymap.config.example.ts` and a minimal `data/places.sample/` dataset to start from.
- `SELF-HOSTING.md`: full walkthrough for running a fork - config, dataset, env vars, optional Supabase setup, deployment, keeping a fork current.
- Saved custom places and a home location for signed-in users: a private pin layer with its own search and city filter, add/edit/delete, and "Nearest to home" distance sorting. Backed by new `user_places`/`user_home` Supabase tables, RLS-scoped to each user.
- Personal calendar deadlines/events for signed-in users, overlaid on the public exam calendar. Backed by a new `user_events` Supabase table, RLS-scoped to each user.
- GitHub Discussions enabled, with a "Request a city" discussion template.
- `CONTRIBUTORS.md` and a documented recognition process (add your handle on your first merged PR).
- `good first issue` / `help wanted` labels applied to the issues that are actually ready to pick up, plus a "Good first issues" README section pointing newcomers at them.
- Vitest + React Testing Library; unit tests for `lib/geo`, `lib/share`, and `lib/places` (`npm run test:unit`).
- `docs/OFFLINE_CACHING.md`: what the PWA service worker caches and how to force a fresh load after a deploy.
- `repair_shop` documented as a valid place type in `CONTRIBUTING.md`, `data/CONTRIBUTING.md`, `README.md`, and the data validator (was a valid `PlaceType` but undocumented and rejected by `scripts/validate-places.mjs`).

### Changed

- `getCities()` now orders cities by `studymap.config.ts`'s `cities` registry, falling back to alphabetical for anything not in that list (previously always alphabetical).

### Fixed

Expand All @@ -30,14 +46,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 17 places that shared identical coordinates are now spread to their true locations.
- Hero section: explicit space rendered between category count and the word "categories".
- Initial tile load optimized: `keepBuffer` and `updateWhenZooming` tuned for faster first paint.
- Saved-places and personal-events UI now surfaces a clear, actionable error when the backend tables haven't been set up yet, instead of silently rendering an empty state.
- Various `react-hooks` lint violations (`set-state-in-effect`, `refs`) across effect-based state resets and a vendored particles component.

### Removed

- `src/lib/constants.ts`, superseded by `studymap.config.ts`.

### Closes issues

- #29 Config-driven region (`studymap.config.ts`)
- #30 Data-source abstraction (configurable places path)
- #31 SELF-HOSTING.md
- #33 Template repo + config.example + sample dataset
- #35 good-first-issue labels + starter board + README section
- #36 CONTRIBUTORS recognition
- #37 Enable Discussions + request-a-city template
- #39 Vitest + RTL unit tests for lib/{geo,share,places}
- #60 Saved custom places + home location (signed-in, private)
- #61 Personal calendar deadlines/events (signed-in, private)
- #62 Bottom sheet / Filters fails to open on real mobile devices
- #63 Map popups auto-close immediately after opening
- #64 Tune marker clustering so unrelated categories don't render as overlapping pins
- #65 Migrate basemap provider for genuine colorful light+dark tiles
- #66 Service worker tile-cache check targets a stale hostname
- #67 Document PWA service-worker caching behaviour + how to force-refresh

## [1.2.2] - 2026-06-29

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Proof goes in the pull request, never in the committed JSON.

## The place record

Each place is one object inside `data/places/<type>.json`. Valid types: `book_shop`, `library`, `exam_centre`, `imp_locations`, `stationery`, `internet_cafe`, `airport`, `train_station`.
Each place is one object inside `data/places/<type>.json`. Valid types: `book_shop`, `library`, `exam_centre`, `imp_locations`, `stationery`, `internet_cafe`, `airport`, `train_station`, `repair_shop`.

```json
{
Expand Down
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ npm install
npm run dev
```

Open http://localhost:3000. No environment variables needed; the map reads place data directly from `data/places/`.
Open http://localhost:3000. No environment variables needed; the map reads place data via `studymap.config.ts`, which imports it from `data/places/`.

## Data schema

Expand All @@ -35,15 +35,16 @@ Places live in `data/places/<type>.json`, one file per category:
| `stationery.json` | `stationery` |
| `internet_cafe.json` | `internet_cafe` |
| `imp_locations.json` | `imp_locations` |
| `repair_shop.json` | `repair_shop` |

Each record shape (`src/lib/types.ts`):

```ts
{
id: string; // kebab-case, unique across all types
name: string;
type: PlaceType; // one of the 8 keys above
city: "mumbai" | "thane" | "navi_mumbai";
type: PlaceType; // one of the 9 keys above
city: string; // free-form slug, e.g. "mumbai", "navi_mumbai" - any city worldwide
lat: number;
lng: number;
address?: string;
Expand All @@ -65,33 +66,45 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines.
## Architecture

```
studymap.config.ts # region + dataset config - the one file a fork edits to retarget StudyMap
src/
app/
page.tsx # homepage (hero + map preview)
map/page.tsx # full interactive map
calendar/ # public exam calendar + signed-in personal events
login/ # optional sign-in (Google OAuth, email)
auth/callback/ # OAuth code exchange
contribute/page.tsx # contribution guide
legal/ # privacy, terms, disclaimer
layout.tsx # root layout (navbar, footer, theme)
components/
home/ # Hero, MapPreview
map/ # PlacesMap, MapView, FilterPanel, NearMeButton
map/ # PlacesMap, MapView, MapPanel, MyPlacesPanel, dialogs
calendar/ # PersonalEventDialog
pins/ # PinPopup
layout/ # Navbar, Footer
lib/
places.ts # getPlaces(), filterPlaces(), city loader
places.ts # getPlaces(), filterPlaces(), getCities() - reads from studymap.config.ts
geo.ts # distance calculation, LatLng type
types.ts # PlaceType, City, Place interface
map.ts # PLACE_TYPE_COLORS, directionsUrl
share.ts # URL state encode/decode for shareable links
site.ts # site metadata, navLinks
user-places.ts # saved places + home location (signed-in only)
user-events.ts # personal calendar events (signed-in only)
data/
places/ # 8 JSON files, one per place type
places/ # 9 JSON files, one per place type
supabase/
migrations/ # SQL for the two optional, sign-in-gated tables (run once by hand)
```

See [ARCHITECTURE.md](ARCHITECTURE.md) for the full breakdown.

## Tech stack

- **Next.js 16** (App Router, static export)
- **Leaflet + react-leaflet** (interactive map)
- **Next.js 16** (App Router)
- **Leaflet + react-leaflet** (interactive map, marker clustering)
- **Supabase** (optional sign-in, gates saved places + personal calendar events)
- **shadcn/ui + Tailwind v4** (UI components)
- **next-themes** (dark/light mode)

Expand Down
1 change: 1 addition & 0 deletions data/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Places live in `data/places/<type>.json`, one file per place type, one JSON obje
| `internet_cafe.json` | Cyber cafes and internet kiosks |
| `airport.json` | Airports |
| `train_station.json` | Railway stations |
| `repair_shop.json` | Phone, laptop, and gadget repair shops |

## Record format

Expand Down
1 change: 1 addition & 0 deletions scripts/validate-places.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const VALID_TYPES = new Set([
"internet_cafe",
"airport",
"train_station",
"repair_shop",
]);

// Valid geographic coordinate ranges. Catches impossible values (e.g. lat=200)
Expand Down
Loading
Loading