Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
3111af2
simple 'useClient' + updaated env var
Alessandro100 Jan 6, 2026
281edd0
more env var name change
Alessandro100 Jan 6, 2026
1c61928
nextjs configs + react update
Alessandro100 Jan 6, 2026
b7c961b
updated to MUI 7.3.6
Alessandro100 Jan 6, 2026
529c7c0
small adjustments
Alessandro100 Jan 6, 2026
b9a4098
footer styling
Alessandro100 Jan 6, 2026
478df83
updated components
Alessandro100 Jan 6, 2026
ef7b952
initial working code
Alessandro100 Jan 6, 2026
772a9d9
configs to deploy nextjs to cloud run
Alessandro100 Jan 7, 2026
c48c7d9
Map away from leaflet (not working)
Alessandro100 Jan 7, 2026
281a0a1
fetching datasets
Alessandro100 Jan 7, 2026
31c5c43
about page as SSR (not quite)
Alessandro100 Jan 7, 2026
9a0ae43
commenting out old code
Alessandro100 Jan 7, 2026
658bb0b
theme updates
Alessandro100 Jan 7, 2026
a62277f
about page ssr friendly (not done looking)
Alessandro100 Jan 7, 2026
2afb96f
updated files with new i18n settings
Alessandro100 Jan 8, 2026
6abc1ea
SSR i18n setup config
Alessandro100 Jan 8, 2026
bf4a0f8
firebase remote config -> to re-look at
Alessandro100 Jan 8, 2026
fdd85ef
associated feeds + structured data
Alessandro100 Jan 8, 2026
c37be9a
updated i18n keys for feedview
Alessandro100 Jan 8, 2026
a331988
updated styling of h1 consistent tag
Alessandro100 Jan 8, 2026
d41aeb6
feeds search page update to nextjs
Alessandro100 Jan 9, 2026
0cb996e
i18n fix
Alessandro100 Jan 9, 2026
57bf260
client access token on init fix
Alessandro100 Jan 9, 2026
b7a29a2
fixed account state
Alessandro100 Jan 9, 2026
75b9e2b
i18n keys common update
Alessandro100 Jan 9, 2026
8407ca0
fixed env usage
Alessandro100 Jan 9, 2026
7a31a1e
bug fixes
Alessandro100 Jan 9, 2026
93f9547
comments and package update
Alessandro100 Jan 12, 2026
cdf3802
feed detail page: routes data
Alessandro100 Jan 13, 2026
92af308
map fixes
Alessandro100 Jan 13, 2026
600969a
multiple datasets loading fix
Alessandro100 Jan 13, 2026
f64a051
feed detail page gtfs rt related feeds fix
Alessandro100 Jan 13, 2026
7352b81
feed detail page gbfs autodiscovery link fix
Alessandro100 Jan 13, 2026
2a38f7c
about page temp styling
Alessandro100 Jan 14, 2026
db361e4
reverted footer to client component to use theme
Alessandro100 Jan 14, 2026
ac3d599
updated readme
Alessandro100 Jan 14, 2026
5bda17d
feed view update and gbfs map
Alessandro100 Jan 14, 2026
5e3ded3
update start prod script
Alessandro100 Jan 15, 2026
e63f8e7
fixed footer displacement
Alessandro100 Jan 15, 2026
cd2703a
i18n keys fix
Alessandro100 Jan 15, 2026
d1e173c
updated eslint rules to nextjs relevant
Alessandro100 Jan 15, 2026
1c5ea2b
lint fixes
Alessandro100 Jan 15, 2026
06b138c
app loading state
Alessandro100 Jan 15, 2026
285cbcc
lint fixes
Alessandro100 Jan 15, 2026
4a1dd44
route hack: feeds/id for backwards compatible
Alessandro100 Jan 15, 2026
6e298b5
gitignore next type gen
Alessandro100 Jan 16, 2026
d68e5bd
documentation
Alessandro100 Jan 16, 2026
12c5c26
cleanup
Alessandro100 Jan 16, 2026
df7535e
map types and cleanup
Alessandro100 Jan 16, 2026
3bf37a9
extra font weights
Alessandro100 Jan 19, 2026
33fcc80
comments and documentation
Alessandro100 Jan 19, 2026
c402813
feeds/id clarification
Alessandro100 Jan 19, 2026
cd911c9
cleanup
Alessandro100 Jan 19, 2026
7a8cc2b
updated test setup
Alessandro100 Jan 19, 2026
c60ed28
fixed tests
Alessandro100 Jan 19, 2026
3fe5a98
cleanup
Alessandro100 Jan 19, 2026
7cdd2a1
e2e test fixes
Alessandro100 Jan 20, 2026
046e51f
e2e config for rsc
Alessandro100 Jan 20, 2026
8c814dc
cleanup
Alessandro100 Jan 20, 2026
a0c909c
lint fix
Alessandro100 Jan 20, 2026
5de645c
lint fixes
Alessandro100 Jan 20, 2026
1e8f257
lint fixes
Alessandro100 Jan 21, 2026
877f723
react types + removal of react-scripts
Alessandro100 Jan 21, 2026
56b4cad
nextjs optimized svg import
Alessandro100 Jan 21, 2026
1d1caa1
gbfs validator env variable fix
Alessandro100 Jan 21, 2026
0e69370
small cleanup
Alessandro100 Jan 21, 2026
c36feb4
removed docker files for cloud run
Alessandro100 Jan 21, 2026
ceacd70
updated gitignore
Alessandro100 Jan 21, 2026
f8be05d
lint fixes
Alessandro100 Jan 21, 2026
27dcf75
package update
Alessandro100 Jan 21, 2026
b4558f6
included Vercel analytics
Alessandro100 Jan 23, 2026
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: 0 additions & 2 deletions .eslintignore

This file was deleted.

69 changes: 37 additions & 32 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard-with-typescript",
"plugin:react/recommended",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": "latest"
},
"plugins": [
"react",
"@typescript-eslint",
"unused-imports",
"prettier"
],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"no-console": "warn",
"unused-imports/no-unused-imports": "error",
"react/react-in-jsx-scope": "off",
"prettier/prettier": "error",
"@typescript-eslint/ban-tslint-comment": "off",
"react/prop-types": "off",
"eqeqeq": "off"
"ignorePatterns": [
"dist/",
"build/",
"node_modules/",
".next/",
"coverage/",
"*.config.js"
],
"env": { "browser": true, "es2021": true },
"extends": [
"standard-with-typescript",
"plugin:react/recommended",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json",
"ecmaFeatures": {
"jsx": true
}
},
"parser": "@typescript-eslint/parser",
"plugins": ["react", "@typescript-eslint", "unused-imports", "prettier"],
"settings": { "react": { "version": "detect" } },
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"unused-imports/no-unused-imports": "error",
"react/react-in-jsx-scope": "off",
"prettier/prettier": "error",
"@typescript-eslint/ban-tslint-comment": "off",
"react/prop-types": "off",
"eqeqeq": "off"
}
}
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ yarn-error.log*
cypress/screenshots
cypress/videos

.env.*
.env
.env.*

.next
next-env.d.ts
.vercel
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}
66 changes: 45 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
# Mobility Feeds API UI

This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
Using node v18.16.0 (npm v9.5.1)
This project is built with [Next.js](https://nextjs.org/) using the App Router.
Using node v24.12.0 (npm v11.6.2) (yarn v1.22.22)

## Installing packages
It is preferred to install the packages using `yarn` over `npm install`

## Configuration variables
React scripts can inject all necessary environment variables into the application in development mode and the JS bundle files.
Next.js can inject all necessary environment variables into the application in development mode and the JS bundle files.
Steps to set environment variables:
- Create a file based on `src/.env.rename_me` with the name `src/.env.{environment}`. Example, `src/.env.dev`.
- Create a file based on `.env.rename_me` with the name `.env.development` (for dev) or `.env` (for prod)
- Replace all key values with the desired content.
- Done! You can now start or build the application with the commands described below.

### Adding a new environment variable
To add a new environment variable, add the variable name to the `src/.env.{environment}` and modify the GitHub actions injecting the value per environment. When adding a new variable, make sure that the variable name is prefixed with `REACT_APP`; otherwise, the react app will not read the variable.
To add a new environment variable, add the variable name to the `.env.{environment}` and modify the GitHub actions injecting the value per environment. When adding a new variable, make sure that the variable name is prefixed with `NEXT_PUBLIC_` for client-side usage; otherwise, the Next.js app will not read the variable on the client side.

## Available Scripts

In the project directory, you can run:

- Runs the app in the development mode:
It will automatically use environment variables located in `.env.development`
```
yarn start:dev
yarn run start:dev
```

- Running a production build
It will build then run the application in production and serve it locally using environment variables located in `.env`
Since it uses the files from the build, it will not hot reload
```
yarn run start:prod
```

Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
Expand All @@ -35,19 +43,19 @@ You will also see any lint errors in the console.
yarn test
```

See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

- Builds the app locally to the `build` folder:
- Builds the app for production:
```
yarn build:dev
yarn build:prod
```

It bundles React in production mode for a target Firebase environment.
It bundles the application in production mode using Next.js optimization.

The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
The build is optimized and ready for deployment.

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
- Start the production build locally:
```
yarn start:prod
```

- Linter check
```
Expand Down Expand Up @@ -82,24 +90,40 @@ npx firebase hosting:channel:deploy {channel_name}
Component and E2E tests are executed with [Cypress](https://docs.cypress.io/). Cypress tests are located in the cypress folder.

Cypress useful commands:
- Run the firebase emulator in a separate terminal
- E2E tests require a mock server to run to mock api endpoints
```
yarn run firebase:auth:emulator:dev
yarn run e2e:setup
```
- Run local headless tests
Will start the dev environment with mock server. It's equal to running
```
yarn start:dev
yarn run firebase:auth:emulator:dev + yarn run start:dev:mock"
```
In a different terminal,
```
yarn cypress-run
yarn e2e:run
```
- Opens Cypress in the interactive GUI
```
yarn cypress-open
yarn e2e:open
```

## API Types Generation

The project includes scripts for generating TypeScript types from OpenAPI specifications:

- Generate API types from main database catalog:
```
yarn generate:api-types
```

- Generate GBFS validator types:
```
yarn generate:gbfs-validator-types
```

## References

- You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
- You can learn more in the [Next.js documentation](https://nextjs.org/docs).
- To learn React, check out the [React documentation](https://reactjs.org/).
- [Firebase Documentation](https://firebase.google.com/docs).
- [next-intl Documentation](https://next-intl-docs.vercel.app/) for internationalization.
3 changes: 0 additions & 3 deletions babel.config.js

This file was deleted.

9 changes: 5 additions & 4 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { defineConfig } from 'cypress';
import * as dotenv from 'dotenv';
const localEnv = dotenv.config({ path: './src/.env.dev' }).parsed;
const ciEnv = dotenv.config({ path: './src/.env.test' }).parsed;
const localEnv = dotenv.config({ path: './.env.development' }).parsed || {};
const ciEnv = dotenv.config({ path: './.env.test' }).parsed || {};

const isEnvEmpty = (obj) => {
return Object.keys(obj).length === 0;
return !obj || Object.keys(obj).length === 0;
};

const chosenEnv = isEnvEmpty(localEnv) ? ciEnv : localEnv;

export default defineConfig({
env: chosenEnv,
e2e: {
baseUrl: 'http://localhost:3000',
// Use CYPRESS_BASE_URL env var if set (for e2e:run/e2e:open), otherwise default to 3000
baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000',
},
video: true,
});
47 changes: 33 additions & 14 deletions cypress/e2e/addFeedForm.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,53 @@ describe('Add Feed Form', () => {
});
cy.visit('/');
cy.get('[data-testid="home-title"]').should('exist');
cy.createNewUserAndSignIn('cypressTestUser@mobilitydata.org', 'BigCoolPassword123!');
cy.createNewUserAndSignIn(
'cypressTestUser@mobilitydata.org',
'BigCoolPassword123!',
);

cy.get('[data-cy="accountHeader"]').should('exist'); // assures that the user is signed in
cy.visit('/contribute');
// 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
cy.get('[data-cy=isOfficialProducerYes]', { timeout: 25000 }).should('exist');
cy.get('[data-cy=isOfficialProducerYes]', { timeout: 25000 }).should(
'exist',
);
});

describe('Success Flows', () => {
it('should submit a new gtfs scheduled feed as official producer', () => {
cy.get('[data-cy=isOfficialProducerYes]').click({ force: true });
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', {
force: true,
});
cy.get('[data-cy=submitFirstStep]').click();
cy.url().should('include', '/contribute?step=2');
// step 2
cy.muiDropdownSelect('[data-cy=countryDropdown]', 'CA');
cy.get('[data-cy=secondStepSubmit]').click();
cy.url().should('include', '/contribute?step=3');
// step 3: fill required emptyLicenseUsage if present
cy.get('body').then($body => {
cy.get('body').then(($body) => {
if ($body.find('[data-cy="emptyLicenseUsage"]').length) {
cy.get('[data-cy="emptyLicenseUsage"]').click();
cy.get('li').should('have.length.at.least', 1);
cy.get('li').then($lis => {
cy.get('li').then(($lis) => {
const texts = $lis.map((i, el) => el.textContent).get();
cy.log('Dropdown options:', texts.join(', '));
expect(texts).to.include('Not sure');
cy.wrap(texts).should('include', 'Not sure');
});
cy.contains('li', 'Not sure').click();
}
});
cy.get('[data-cy=thirdStepSubmit]').click();
cy.url().should('include', '/contribute?step=4');
// step 4
cy.get('[data-cy=dataProducerEmail] input').type('audio@stm.com', { force: true });
cy.get('[data-cy=dataProducerEmail] input').type('audio@stm.com', {
force: true,
});
cy.muiDropdownSelect('[data-cy=interestedInAudit]', 'no');
cy.muiDropdownSelect('[data-cy=logoPermission]', 'yes');
cy.get('[data-cy=fourthStepSubmit]').click();
Expand Down Expand Up @@ -86,7 +95,9 @@ describe('Add Feed Form', () => {
// Step 1 values
cy.get('[data-cy=isOfficialProducerYes]').click();
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', {
force: true,
});
cy.get('[data-cy=oldFeedLink] input').type('https://example.com/feedOld');
cy.get('[data-cy=submitFirstStep]').click();
// Step 2
Expand Down Expand Up @@ -135,19 +146,27 @@ describe('Add Feed Form', () => {
cy.get('[data-cy=unofficialDesc]').should('exist');
cy.get('[data-cy=updateFreq]').should('exist');
// Fill in the new fields (ensure only one element is targeted)
cy.get('[data-cy=unofficialDesc] textarea').first().type('For research purposes', { force: true });
cy.get('[data-cy=updateFreq] input').first().type('every month', { force: true });
cy.get('[data-cy=unofficialDesc] textarea')
.first()
.type('For research purposes', { force: true });
cy.get('[data-cy=updateFreq] input')
.first()
.type('every month', { force: true });
// Continue with the rest of the form
cy.muiDropdownSelect('[data-cy=dataType]', 'gtfs');
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', {
force: true,
});
cy.get('[data-cy=submitFirstStep]').click();
cy.url().should('include', '/contribute?step=2');
});

it('should show and require emptyLicenseUsage with Unsure option if official producer and no license', () => {
cy.get('[data-cy=isOfficialProducerYes]').click();
cy.muiDropdownSelect('[data-cy=isOfficialFeed]', 'yes');
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', { force: true });
cy.get('[data-cy=feedLink] input').type('https://example.com/feed', {
force: true,
});
cy.get('[data-cy=submitFirstStep]').click();
cy.url().should('include', '/contribute?step=2');
// step 2: leave license blank
Expand All @@ -163,11 +182,11 @@ describe('Add Feed Form', () => {
// Open dropdown and check options with debug output
cy.get('[data-cy="emptyLicenseUsage"]').click();
cy.get('li').should('have.length.at.least', 1);
cy.get('li').then($lis => {
cy.get('li').then(($lis) => {
const texts = $lis.map((i, el) => el.textContent).get();
// Debug output
cy.log('Dropdown options:', texts.join(', '));
expect(texts).to.include('Not sure');
cy.wrap(texts).should('include', 'Not sure');
});
cy.contains('li', 'Not sure').click();
cy.get('[data-cy="thirdStepSubmit"]').click();
Expand Down
Loading