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
2 changes: 1 addition & 1 deletion cypress/e2e/addFeedForm.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('Add Feed Form', () => {
);

cy.get('[data-cy="accountHeader"]').should('exist'); // assures that the user is signed in
cy.visit('/contribute');
cy.get('[data-cy="header-add-a-feed"]').click();
// Assures that the firebase remote config has loaded for the first test
// Optimizations can be made to make the first test run faster
// Long timeout is to assure no flakiness
Expand Down
7 changes: 4 additions & 3 deletions cypress/e2e/changepassword.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ describe('Change Password Screen', () => {
cy.visit('/');
cy.get('[data-testid="home-title"]').should('exist');
cy.createNewUserAndSignIn(email, currentPassword);
cy.get('[data-cy="accountHeader"]').should('exist'); // assures that the user is signed in
cy.visit('/change-password');
cy.get('[data-cy="accountHeader"]').should('exist').click(); // assures that the user is signed in
cy.get('[data-cy="accountDetailsHeader"]').should('exist').click();
cy.get('[data-cy="changePasswordButton"]').should('exist').click();
});

it('should render components', () => {
Expand Down Expand Up @@ -51,7 +52,7 @@ describe('Change Password Screen', () => {

// logout
cy.get('[data-cy="signOutButton"]').click();
cy.get('[data-cy="confirmSignOutButton"]').should('exist').click();
cy.get('[data-cy="confirmSignOutButton"]').should('exist').should('not.be.disabled').click();
cy.visit('/sign-in');
cy.get('[data-cy="signInEmailInput"]').type(email);
cy.get('[data-cy="signInPasswordInput"]').type(newPassword);
Expand Down
43 changes: 43 additions & 0 deletions docs/ssg-initial-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
```mermaid
sequenceDiagram
autonumber
actor U as User
participant B as Browser
participant CDN as CDN/Edge Cache
participant Next as Next.js Build (CI)
participant I18N as next-intl (generateStaticParams)
participant RC as Firebase Remote Config (build-time)
participant JS as Client JS (Header component)

rect rgb(235, 245, 255)
note over Next,RC: BUILD TIME (SSG)
Next->>I18N: generateStaticParams() → all locales
I18N-->>Next: locale list
Next->>I18N: Build locale message bundles (baked)
Next->>RC: Fetch Remote Config snapshot (baked)
RC-->>Next: Remote Config values
Next->>Next: Render pages to static HTML
Next->>Next: Extract critical MUI styles (Emotion)
note over Next: <style data-emotion="mui-*"> inlined in HTML
Next->>Next: Emit JS/CSS assets (Header client bundle)
Next->>CDN: Deploy static HTML + assets
end

rect rgb(245, 245, 245)
note over U,CDN: RUNTIME (User request)
U->>B: Navigate to /{locale}/page
B->>CDN: GET /{locale}/page
CDN-->>B: 200 Static HTML (SSG + inline MUI styles)

note over B: ✅ Styled content is painted\n(LCP occurs here)

B->>CDN: GET external CSS (non-critical)
CDN-->>B: CSS files
B->>CDN: GET JS bundle(s)
CDN-->>B: JS

note over B: React hydration begins
B->>JS: Hydrate client components
note over JS: ✅ Header hydration completes\n(interactivity enabled)
end
```
32 changes: 32 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -471,5 +471,37 @@
"nearby_stations": "Nearby Stations"
},
"unableToDetectVersions": "Unable to detect versions within this feed."
},
"home": {
"title": "Explore and Access Global Transit Data",
"servingOver": "Currently serving over",
"feeds": "transportation data feeds from over",
"fromOver": "from over",
"countries": "countries.",
"or": "or",
"browseFeeds": "Browse Feeds",
"addFeed": "Add a feed",
"signUpApi": "Sign up for the API",
"description": "The Mobility Database is an open data catalog including over 4000 GTFS, GTFS Realtime, and GBFS feeds in over 75 countries. Whether you're a transportation operator, a researcher studying public transit and shared mobility trends, or a maps app needing reliable data to use with your application, the Mobility Database has everything you need in one central location.",
"validatorIntro": "Our database integrates with",
"gtfsValidator": "the Canonical GTFS Schedule Validator",
"and": "and",
"gbfsValidator": "the GBFS Validator",
"validatorOutro": "to provide detailed data quality reports on every feed."
},
"about": {
"title": "About",
"description": "The Mobility Database is an open catalog including over 4000 GTFS, GTFS Realtime, and GBFS feeds in over 75 countries. It integrates with the Canonical GTFS Schedule and GBFS Validators to share data quality reports for each feed.\n\nThis database is hosted and maintained by MobilityData, the global non-profit organization dedicated to the advancement of open transportation data standards.",
"learnMore": "Learn more about MobilityData",
"whyUse": "Why Use the Mobility Database?",
"whyUseAnswer": "The Mobility Database provides free access to historical and current GTFS, GTFS Realtime, and GBFS feeds from around the world. These feeds are checked for updates every day, ensuring that the data you're looking at is the most recent data available.",
"gtfsValidator": "the Canonical GTFS Schedule Validator",
"gbfsValidator": "the GBFS Validator.",
"benefits": {
"mirrored": "Mirrored versions of operator-hosted GTFS Schedule feeds to avoid operator website downtimes and geoblocking",
"boundingBoxes": "Bounding boxes that help to visualize or filter in the API by a select region",
"addFeeds": "A simple, easy-to-use form to add new feeds",
"openSource": "An open source community actively working to improve the tools"
}
}
}
32 changes: 32 additions & 0 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -471,5 +471,37 @@
"nearby_stations": "Nearby Stations"
},
"unableToDetectVersions": "Unable to detect versions within this feed."
},
"home": {
"title": "Explorez et accédez aux données de transport mondiales",
"servingOver": "Actuellement plus de",
"feeds": "flux de données de transport de plus de",
"fromOver": "de plus de",
"countries": "pays.",
"or": "ou",
"browseFeeds": "Parcourir les flux",
"addFeed": "Ajouter un flux",
"signUpApi": "S'inscrire à l'API",
"description": "La Mobility Database est un catalogue de données ouvertes comprenant plus de 4000 flux GTFS, GTFS Realtime et GBFS dans plus de 75 pays. Que vous soyez un opérateur de transport, un chercheur étudiant les tendances du transport public et de la mobilité partagée, ou une application de cartes ayant besoin de données fiables, la Mobility Database a tout ce dont vous avez besoin en un seul endroit.",
"validatorIntro": "Notre base de données s'intègre avec",
"gtfsValidator": "le validateur canonique GTFS Schedule",
"and": "et",
"gbfsValidator": "le validateur GBFS",
"validatorOutro": "pour fournir des rapports détaillés sur la qualité des données de chaque flux."
},
"about": {
"title": "À propos",
"description": "La Mobility Database est un catalogue ouvert comprenant plus de 4000 flux GTFS, GTFS Realtime et GBFS dans plus de 75 pays. Elle s'intègre avec les validateurs canoniques GTFS Schedule et GBFS pour partager des rapports de qualité des données pour chaque flux.\n\nCette base de données est hébergée et maintenue par MobilityData, l'organisation mondiale à but non lucratif dédiée à l'avancement des standards de données de transport ouvertes.",
"learnMore": "En savoir plus sur MobilityData",
"whyUse": "Pourquoi utiliser la Mobility Database ?",
"whyUseAnswer": "La Mobility Database fournit un accès gratuit aux flux GTFS, GTFS Realtime et GBFS historiques et actuels du monde entier. Ces flux sont vérifiés quotidiennement pour les mises à jour, garantissant que les données que vous consultez sont les plus récentes disponibles.",
"gtfsValidator": "le validateur canonique GTFS Schedule",
"gbfsValidator": "le validateur GBFS.",
"benefits": {
"mirrored": "Versions miroirs des flux GTFS Schedule hébergés par les opérateurs pour éviter les temps d'arrêt et le blocage géographique",
"boundingBoxes": "Boîtes englobantes pour visualiser ou filtrer par région sélectionnée dans l'API",
"addFeeds": "Un formulaire simple et facile à utiliser pour ajouter de nouveaux flux",
"openSource": "Une communauté open source travaillant activement à améliorer les outils"
}
}
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"react-draggable": "^4.5.0",
"react-ga4": "^2.1.0",
"react-google-recaptcha": "^3.1.0",
"react-helmet-async": "^2.0.5",
"react-hook-form": "^7.52.1",
"react-leaflet": "^4.2.1",
"react-map-gl": "^8.0.4",
Expand All @@ -56,6 +55,7 @@
},
"scripts": {
"build:prod": "next build",
"build:analyze": "next experimental-analyze",
"start:dev": "next dev",
"start:dev:mock": "NEXT_PUBLIC_API_MOCKING=enabled next dev -p 3001",
"start:prod": "next build && next start",
Expand All @@ -73,6 +73,9 @@
"generate:gbfs-validator-types:output": "npm exec -- openapi-typescript ./external_types/GbfsValidator.yaml -o $npm_config_output_path && eslint $npm_config_output_path --fix",
"generate:gbfs-validator-types": "npm run generate:gbfs-validator-types:output -- --output-file=src/app/services/feeds/gbfs-validator-types.ts"
},
"resolutions": {
"tar": "^7.5.7"
},
"eslintConfig": {
"extends": [
"react-app",
Expand Down
57 changes: 36 additions & 21 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,44 @@

import './App.css';
import AppRouter from './router/Router';
import { BrowserRouter } from 'react-router-dom';
import { MemoryRouter } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { anonymousLogin } from './store/profile-reducer';
import { app } from '../firebase';
import { Suspense, useEffect, useState } from 'react';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import AppContainer from './AppContainer';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { usePathname, useSearchParams } from 'next/navigation';

function App(): React.ReactElement {
interface AppProps {
locale?: string;
}

// Helper function to construct the full path from Next.js routing
function buildPathFromNextRouter(
pathname: string,
searchParams: URLSearchParams,
locale?: string,
): string {
const cleanPath =
locale != null && locale !== 'en'
? (pathname.replace(`/${locale}`, '') ?? '/')
: pathname;

const searchString = searchParams.toString();
return searchString !== '' ? `${cleanPath}?${searchString}` : cleanPath;
}
Comment on lines +20 to +32
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSearchParams() from next/navigation returns ReadonlyURLSearchParams, not URLSearchParams, so this call is likely a TypeScript error. Also, pathname.replace(\/${locale}`, '')will replace the first occurrence anywhere in the string rather than only stripping a leading locale segment; using a prefix check (e.g.,pathname.startsWith(...)`) avoids accidental replacements.

Copilot uses AI. Check for mistakes.

function App({ locale }: AppProps): React.ReactElement {
const dispatch = useDispatch();
const [isAppReady, setIsAppReady] = useState(false);

const pathname = usePathname();
const searchParams = useSearchParams();

const initialPath = buildPathFromNextRouter(pathname, searchParams, locale);
Comment on lines +38 to +41
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSearchParams() from next/navigation returns ReadonlyURLSearchParams, not URLSearchParams, so this call is likely a TypeScript error. Also, pathname.replace(\/${locale}`, '')will replace the first occurrence anywhere in the string rather than only stripping a leading locale segment; using a prefix check (e.g.,pathname.startsWith(...)`) avoids accidental replacements.

Copilot uses AI. Check for mistakes.

useEffect(() => {
app.auth().onAuthStateChanged((user) => {
if (user != null) {
Expand All @@ -29,24 +53,15 @@ function App(): React.ReactElement {
}, [dispatch]);

return (
<HelmetProvider>
<Helmet>
<meta
name='description'
content={
"Access GTFS, GTFS Realtime, GBFS transit data with over 4,000 feeds from 70+ countries on the web's leading transit data platform."
}
/>
</Helmet>
<Suspense>
<LocalizationProvider dateAdapter={AdapterDayjs}>
{/* BrowserRouter will be deprecated in favor of Next AppRouter */}
<BrowserRouter>
<AppContainer>{isAppReady ? <AppRouter /> : null}</AppContainer>
</BrowserRouter>
</LocalizationProvider>
</Suspense>
</HelmetProvider>
<Suspense>
<LocalizationProvider dateAdapter={AdapterDayjs}>
{/* MemoryRouter will be deprecated in favor of Next AppRouter */}
{/* MemoryRouter synced with Next.js routing via RouterSync component */}
<MemoryRouter initialEntries={[initialPath]}>
<AppContainer>{isAppReady ? <AppRouter /> : null}</AppContainer>
</MemoryRouter>
</LocalizationProvider>
</Suspense>
);
}

Expand Down
5 changes: 0 additions & 5 deletions src/app/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,19 @@ import * as React from 'react';
import { Box, LinearProgress } from '@mui/material';
import type ContextProviderProps from './interface/ContextProviderProps';
import { useLocation } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import { selectLoadingApp } from './store/selectors';
import { useSelector } from 'react-redux';

const AppContainer: React.FC<ContextProviderProps> = ({ children }) => {
const isAppLoading = useSelector(selectLoadingApp);
const location = useLocation();
const canonicalUrl = window.location.origin + location.pathname;

React.useLayoutEffect(() => {
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
}, [location.pathname]);

return (
<>
<Helmet>
<link rel='canonical' href={canonicalUrl} />
</Helmet>
<Box id='app-main-container'>
{isAppLoading ? (
<Box sx={{ width: '100%', mt: '-31px' }}>
Expand Down
12 changes: 0 additions & 12 deletions src/app/[[...slug]]/page.tsx

This file was deleted.

40 changes: 40 additions & 0 deletions src/app/[locale]/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

// This page is temporary to ease the migration to Next.js App Router
// It will be deprecated once the migration is fully complete
import { type ReactNode, use, useEffect } from 'react';
import dynamic from 'next/dynamic';
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';
import { store } from '../../store/store';
import { useAppDispatch } from '../../hooks';
import { resetProfileErrors } from '../../store/profile-reducer';

const App = dynamic(async () => await import('../../App'), { ssr: false });

const persistor = persistStore(store);

interface PageProps {
params: Promise<{
locale: string;
slug: string[];
}>;
}

export default function Page({ params }: PageProps): ReactNode {
const { locale } = use(params);
const pathKey = use(params).slug?.join('/') ?? '/';
Comment on lines +24 to +26
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semicolon after <App ... /> is inside JSX and will cause a syntax/parsing error in TSX. Remove the trailing ;. While you’re here, avoid calling use(params) twice—resolve it once into a local variable and read both locale and slug from that object.

Copilot uses AI. Check for mistakes.
const dispatch = useAppDispatch();

useEffect(() => {
// Clean errors from previous session
dispatch(resetProfileErrors());
}, [dispatch]);

// Pass locale to App so BrowserRouter can use correct basename
return (
<PersistGate loading={null} persistor={persistor}>
<App locale={locale} key={pathKey} />;
</PersistGate>
);
}
Comment on lines +35 to +40
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semicolon after <App ... /> is inside JSX and will cause a syntax/parsing error in TSX. Remove the trailing ;. While you’re here, avoid calling use(params) twice—resolve it once into a local variable and read both locale and slug from that object.

Copilot uses AI. Check for mistakes.
Loading