This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Languages of New York City Map (https://languagemap.nyc/) — an interactive Mapbox-driven visualization of NYC's linguistic diversity using data from the Endangered Language Alliance. Originally CRA, now Vite. Hosted on Netlify.
yarn start— Vite dev server at http://localhost:3000yarn build— type-check (tsc --noEmit) thenvite buildto./buildyarn preview— preview the production buildyarn lint—eslint '*/**/*.{js,ts,tsx}' --quiet- No test runner is wired up in
scripts;*.test.tsxfiles exist (e.g.App.test.tsx,Map.test.tsx) but are excluded fromtsconfig.jsonand ESLint, so they are stale. Don't assume they run.
Required env vars (prefix REACT_APP_ is preserved by Vite via envPrefix in vite.config.mts; access as import.meta.env.REACT_APP_*, not process.env):
REACT_APP_MB_TOKEN— Mapbox access token (required)REACT_APP_AIRTABLE_API_KEY— Airtable API key (required; nearly all config + content lives in Airtable)REACT_APP_YOUTUBE_API_KEY— optional, only for video metadataREACT_APP_SENTRY_ENVIRONMENT— optional, set innetlify.tomlper deploy context
Copy sample.env to .env. The hardcoded Airtable base IDs (AIRTABLE_BASE, AIRTABLE_CENSUS_BASE) live in src/components/config/api.ts.
Two external systems drive the app, and the code is tightly coupled to their schemas:
- Mapbox tilesets — layer names
mb-data(language points),puma,tract,counties,neighborhoods. These names appear as object keys throughoutsrc/components/map/and renaming them in Mapbox will break the app in ways TypeScript will not catch. - Airtable — most routes, table names, and column names are referenced by string literals from
src/components/config/api.tsand typed insrc/components/context/types.ts. Seedocs/data-structure.mdfor required fields.
When adding fields, update both the Airtable schema and the TypeScript types in src/components/context/types.ts.
src/index.tsx → ProvidersWrap (theme + GlobalContext + SymbAndLabelContext) → BrowserRouter → App (Sentry boundary + app-wide QueryClientProvider + MapToolsProvider + PanelContextProvider) → AppWrap (layout: PanelWrap, Map, TopBar, BottomNav, plus ResultsModal).
State is split across multiple React Contexts, not Redux:
GlobalContext(src/components/context/GlobalContext.tsx) — language features + filter state viauseReducer(actions inreducer.tsx, types intypes.ts).SymbAndLabelContext— map symbology and label settings.MapToolsContext— basemap, geocoder, census active field, etc.PanelContext(src/components/panels/PanelContext.tsx) — open/closed panel UI state.
Server state uses @tanstack/react-query v4. (The react-query v2 entry in package.json is leftover and unused.) The app-wide QueryClient is created in src/components/App.tsx; its defaults come from reactQueryDefaults in src/components/config/api.ts — everything is staleTime: Infinity, no refetch on focus/mount/reconnect, so Airtable data is treated as effectively static for the session. InfoPanel and MediaModal swap in their own clients (wpQueryClient, mediaQueryClient) for separate cache lifecycles on WordPress/media data.
All routes are declared in routes in src/components/config/api.ts and consumed via <Routes>/<Route> (react-router-dom v6). URL is the source of truth for "what's selected" (e.g. /Explore/Language/:value/:id, /Census/:table/:field/:id). The map listens for route changes to fly/zoom/highlight.
v6 nested-Routes gotcha — read this before adding routes inside a panel. PanelWrap.tsx builds parent routes as ${rootPath}/* from nonNavRoutesConfig, so panel components like InfoPanel and DetailsPanel are children of a parent <Route path="/Foo/*">. Inside a nested <Routes>, v6 strips the parent's pathnameBase from the URL before matching — meaning absolute child paths like <Route path={routes.info}> (= /Info) silently fail to match because the inner basename is already /Info. Use <Route index> for the parent path itself and relative paths (path="About", not path="/Info/About") for siblings. The mechanical v5→v6 migration left a few absolute-path regressions of this shape — git grep '<Route path={routes\.' to find suspects.
Map.tsxis thereact-map-gl<MapGL>host. Children layers (LangMbSrcAndLayer,CensusLayer,PolygonLayerfor neighborhoods/counties) attach Mapbox sources and style layers.- Layer config is split across
config.ts,config.points.ts,config.census.ts,config.non-census-poly.ts— three Mapbox Studio style IDs (light/dark/dark-bg) live inconfig.tsand are used by the basemap switcher. events.tsholds hover/click handlers;hooks.tsxandhooks.points.tshold map-effect hooks (e.g.useFlyToFilteredFeats).
src/components/results/ was migrated from @mui/x-data-grid to @tanstack/react-table (recent commit 6619cb9). ResultsTable.tsx builds the table; config.tsx defines columns + initialColumnVisibility; exporting.ts handles CSV (filefy) and PDF (jspdf + jspdf-autotable) export, which intentionally excludes hidden columns.
The codebase uses MUI v5 (@mui/material) but still relies on the deprecated @mui/styles (makeStyles/createStyles) pattern extensively, plus StyledEngineProvider injectFirst so JSS wins over emotion. The current branch is agupta/upgrade-phase-4-mui-v5 — an in-progress migration. New components should match the surrounding file's style choice rather than introducing a third pattern.
tsconfig.json sets baseUrl: "src", so imports like import { App } from 'components' and import { routes } from 'components/config/api' are absolute-from-src. vite-tsconfig-paths reproduces this for the bundler. Don't add src/ prefixes.
- React 17,
react-router-domv6 (<Routes>/<Route element={...} />/useNavigate),@tanstack/react-queryv4. mapbox-glis pinned to v1.x andreact-map-glto v5.x — the v2/v6+ APIs (Mapfrom react-map-gl v7) are not available.- ESLint extends
airbnb-typescriptwith prettier; lots of custom whitespace/padding rules. Runyarn lintbefore assuming a change is clean. - Husky + lint-staged auto-fix on commit (
eslint --quiet --cache --fix).