|
| 1 | +# Mobility Database Web - AI Agent Instructions |
| 2 | + |
| 3 | +## Project Overview |
| 4 | +Next.js 16 (App Router) web application for browsing and managing mobility transit feeds (GTFS, GTFS-RT, GBFS). Uses Firebase Auth, Redux Toolkit for state management, Material-UI (MUI), and TypeScript. Internationalized with next-intl (English/French). |
| 5 | + |
| 6 | +## Architecture Patterns |
| 7 | + |
| 8 | +### Hybrid Next.js Architecture (Migration in Progress) |
| 9 | +- **Next.js App Router**: New pages in `src/app/` with Server Components by default ([layout.tsx](src/app/layout.tsx), [feeds/[feedDataType]/page.tsx](src/app/feeds/[feedDataType]/page.tsx)) |
| 10 | +- **Legacy React Router**: Still active via `<BrowserRouter>` in [App.tsx](src/app/App.tsx) - **will be deprecated** |
| 11 | +- When adding features, prefer App Router patterns over React Router |
| 12 | + |
| 13 | +### Client vs Server Components |
| 14 | +- **Server Components** (default): Data fetching, API calls, Firebase Admin operations. No `'use client'` directive |
| 15 | +- **Client Components**: Interactive UI with hooks, state, events. Mark with `'use client'` at top ([providers.tsx](src/app/providers.tsx), [Header.tsx](src/app/components/Header.tsx)) |
| 16 | +- **Rule**: Server actions and data fetching happen in server components or API services, client components handle interactivity |
| 17 | + |
| 18 | +### API Integration via OpenAPI-Fetch |
| 19 | +- Type-safe API client generated from OpenAPI specs in `external_types/` |
| 20 | +- Main client: [src/app/services/feeds/index.ts](src/app/services/feeds/index.ts) - uses `openapi-fetch` with auto-typed paths |
| 21 | +- Generate types: `yarn generate:api-types` (DatabaseCatalogAPI) or `yarn generate:gbfs-validator-types` |
| 22 | +- Always use generated types from `services/feeds/types.ts` - never duplicate API response types |
| 23 | +- Auth via Bearer token middleware: `generateAuthMiddlewareWithToken(accessToken)` |
| 24 | + |
| 25 | +### Firebase Authentication Patterns |
| 26 | +- **Client-side**: Firebase compat SDK ([firebase.ts](src/firebase.ts)) with emulator support for Cypress |
| 27 | +- **Server-side**: Firebase Admin SDK ([firebase-admin.ts](src/lib/firebase-admin.ts)) for token verification |
| 28 | +- Auth state managed via Redux ([profile-reducer.ts](src/app/store/profile-reducer.ts)) with status: `authenticated|unauthenticated|anonymous_login|...` |
| 29 | +- Access tokens: `getSSRAccessToken()` for server, Redux state for client |
| 30 | +- Cypress tests auto-use Firebase emulator on port 9099 |
| 31 | + |
| 32 | +### Internationalization (i18n) |
| 33 | +- Next-intl with messages in `messages/{en,fr}.json` |
| 34 | +- Use `useTranslations('namespace')` in client components: `const t = useTranslations('feeds')` |
| 35 | +- Server components: import from `next-intl/server` - `getLocale()`, `getMessages()` |
| 36 | +- Locale from subdomain: `fr.mobilitydatabase.org` → French, else English ([config.ts](src/i18n/config.ts)) |
| 37 | + |
| 38 | +### State Management |
| 39 | +- **Redux Toolkit**: Global state for auth, profile ([store/profile-reducer.ts](src/app/store/profile-reducer.ts)) |
| 40 | +- **React Context**: Theme, Remote Config ([providers.tsx](src/app/providers.tsx)) |
| 41 | +- **Server-side**: React `cache()` for per-request memoization ([remote-config.server.ts](src/lib/remote-config.server.ts)) |
| 42 | + |
| 43 | +### Firebase Remote Config |
| 44 | +- Server-side fetch cached for 5 min (dev) / 1 hour (prod) in [remote-config.server.ts](src/lib/remote-config.server.ts) |
| 45 | +- Passed from server → client via `<Providers remoteConfig={...}>` in [layout.tsx](src/app/layout.tsx) |
| 46 | +- Default values in `src/app/interface/RemoteConfig.ts` |
| 47 | + |
| 48 | +## Key Conventions |
| 49 | + |
| 50 | +### Material-UI (MUI) Usage |
| 51 | +- Use direct imports: `import { Box, Typography } from '@mui/material'` (NOT barrel file `@mui/material/*`) |
| 52 | +- Theme via Emotion + `ThemeRegistry` in [registry.tsx](src/app/registry.tsx) |
| 53 | +- Fonts loaded via next/font: Mulish (body), IBM Plex Mono (mono) defined in [layout.tsx](src/app/layout.tsx) |
| 54 | + |
| 55 | +### File Organization |
| 56 | +- **Screens**: `src/app/screens/{ScreenName}/` - page-level components |
| 57 | +- **Components**: `src/app/components/` - shared UI components |
| 58 | +- **Services**: `src/app/services/` - API clients and external integrations |
| 59 | +- **Utils**: `src/app/utils/` - helper functions (config, auth, formatting) |
| 60 | +- **Functions files**: Logic separated into `*.functions.tsx` ([Feed.functions.tsx](src/app/screens/Feed/Feed.functions.tsx)) |
| 61 | + |
| 62 | +### Testing Strategy |
| 63 | +- **Unit Tests**: Jest + React Testing Library (files: `*.spec.tsx` or `*.test.tsx`) |
| 64 | +- **E2E Tests**: Cypress in `cypress/e2e/` with MSW mocks in [src/mocks/handlers.ts](src/mocks/handlers.ts) |
| 65 | +- Run E2E: `yarn e2e:setup` (starts dev + Firebase emulator + MSW), then `yarn e2e:run` or `yarn e2e:open` |
| 66 | +- Mock API responses using fixtures from `cypress/fixtures/` |
| 67 | + |
| 68 | +### Environment Variables |
| 69 | +- Prefix with `NEXT_PUBLIC_` for client-side access |
| 70 | +- Dev env: `.env.development`, prod: `.env`, CI: `.env.test` |
| 71 | +- Key vars: `NEXT_PUBLIC_FEED_API_BASE_URL`, `NEXT_PUBLIC_FIREBASE_*`, `NEXT_PUBLIC_API_MOCKING` |
| 72 | +- Mock mode: `NEXT_PUBLIC_API_MOCKING=enabled yarn start:dev:mock` (port 3001) |
| 73 | + |
| 74 | +## Development Workflows |
| 75 | + |
| 76 | +### Starting Development |
| 77 | +```bash |
| 78 | +yarn install # Prefer yarn over npm |
| 79 | +yarn start:dev # Dev server on :3000 with hot reload |
| 80 | +yarn start:dev:mock # Dev server with MSW mocks on :3001 |
| 81 | +``` |
| 82 | + |
| 83 | +### Testing Commands |
| 84 | +```bash |
| 85 | +yarn test # Unit tests |
| 86 | +yarn test:watch # Watch mode |
| 87 | +yarn e2e:setup # Start dev + Firebase emulator for E2E |
| 88 | +yarn e2e:open # Cypress interactive mode |
| 89 | +``` |
| 90 | + |
| 91 | +### Building & Deploying |
| 92 | +```bash |
| 93 | +yarn build:prod # Production build (standalone output) |
| 94 | +yarn start:prod # Build + start locally |
| 95 | +yarn lint # ESLint check |
| 96 | +yarn lint:fix # Auto-fix linting issues |
| 97 | +``` |
| 98 | + |
| 99 | +### Regenerating API Types |
| 100 | +```bash |
| 101 | +yarn generate:api-types # DatabaseCatalogAPI → types.ts |
| 102 | +yarn generate:gbfs-validator-types # GbfsValidator → gbfs-validator-types.ts |
| 103 | +``` |
| 104 | +Run after updating OpenAPI specs in `external_types/` |
| 105 | + |
| 106 | +## Common Patterns |
| 107 | + |
| 108 | +### Data Fetching in Server Components |
| 109 | +```tsx |
| 110 | +// pages fetch data directly with access token |
| 111 | +export default async function FeedPage({ params }) { |
| 112 | + const accessToken = await getSSRAccessToken(); |
| 113 | + const feed = await getFeed(feedId, accessToken); |
| 114 | + return <FeedDetails feed={feed} />; |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +### Parallel Data Loading |
| 119 | +Use `Promise.all` in server components ([layout.tsx](src/app/layout.tsx)): |
| 120 | +```tsx |
| 121 | +const [locale, messages, remoteConfig] = await Promise.all([ |
| 122 | + getLocale(), getMessages(), getRemoteConfigValues() |
| 123 | +]); |
| 124 | +``` |
| 125 | + |
| 126 | +### Client Component with Translations |
| 127 | +```tsx |
| 128 | +'use client'; |
| 129 | +import { useTranslations } from 'next-intl'; |
| 130 | + |
| 131 | +export function MyComponent() { |
| 132 | + const t = useTranslations('feeds'); // namespace from messages/ |
| 133 | + return <div>{t('labelKey')}</div>; |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +### Conditional Rendering for Auth |
| 138 | +Redux state for auth checks in client components, check `status` from profile reducer. |
| 139 | + |
| 140 | +## Critical Notes |
| 141 | + |
| 142 | +- **Route hack**: `/feeds/[feedDataType]` actually receives `feedId` for backward compatibility ([page.tsx](src/app/feeds/[feedDataType]/page.tsx)) - redirects to proper route |
| 143 | +- **TypeScript strict mode enabled**: Handle nullish values explicitly |
| 144 | +- **Cypress uses Firebase emulator**: Tests run against auth emulator, not production Firebase |
| 145 | +- **Server-only code**: Mark with `import 'server-only'` for server utils ([remote-config.server.ts](src/lib/remote-config.server.ts)) |
| 146 | +- **Jest transform exceptions**: Some node_modules need manual transformation - see `transformIgnorePatterns` in [jest.config.ts](jest.config.ts) |
| 147 | + |
| 148 | +## Resources |
| 149 | +- Node v24.12.0 (npm v11.6.2, yarn v1.22.22) |
| 150 | +- [Next.js App Router docs](https://nextjs.org/docs/app) |
| 151 | +- [next-intl docs](https://next-intl-docs.vercel.app/) |
| 152 | +- [openapi-fetch](https://openapi-ts.dev/openapi-fetch/) |
| 153 | + |
| 154 | + |
| 155 | +Always load and apply the following project agent skills when reviewing or generating code: |
| 156 | + |
| 157 | +- vercel-react-best-practices (from .github/skills/vercel-react-best-practices) |
| 158 | + |
| 159 | +If the skill is available, prefer its guidance over generic Copilot heuristics. |
| 160 | +If it is not available, emulate its rules as closely as possible. |
| 161 | + |
| 162 | +Acknowledge when the skill is applied. |
0 commit comments