This is the frontend application for Monbo, built with Next.js 15, a powerful React framework for production.
apps/web/
├── public/ # Static files
├── src/
│ ├── api/ # API client and services; schema.d.ts is generated from the API
│ ├── app/ # Next.js app router pages
│ ├── components/ # Reusable React components
│ ├── config/ # Configuration files
│ ├── content/ # Markdown of the documentation page, per language
│ ├── context/ # React context providers
│ ├── hooks/ # Custom React hooks
│ ├── interfaces/ # Aliases of the generated API types, plus frontend-only types
│ ├── locales/ # i18n translation files
│ └── utils/ # Utility functions
There are many ways to run the frontend application. In any case the frontend will be available at http://localhost:3000.
The pnpm version is pinned via the
packageManagerfield inpackage.json, so tools like Corepack andpnpm/action-setupuse a consistent version. You can also run the frontend from the repo root via the orchestrator (pnpm dev,pnpm lint,pnpm build) — see the root README.
You can run the frontend in a Docker container in development mode. The source code will be mounted as a docker volume. This approach supports hot-reloading.
First, you need to create a .env or .env.development file in the apps/web directory containing the environment variables (please use the .env.development.example file as a template).
Please follow the file name convention, because it is used by Nextjs to load them automatically.
Then, execute the following command:
cd apps/web
docker build -f Dockerfile.dev -t monbo-front-dev .
docker run -d -p 3000:3000 --name monbo-front-dev-container -v $(pwd):/app monbo-front-devYou can build and run the productionimage in a Docker container. Note that this approach does not support hot-reloading.
First, you need to create a file in the apps/web directory containing the environment variables (please use the .env.production.example file as a template). In this case, the file name convention is not required. Then, execute the following command:
cd apps/web
docker build -f Dockerfile.prod -t monbo-front-prod .
docker run -d --env-file <env-file-relative-path> -p 3000:3000 --name monbo-front-prod-container monbo-front-prodIf you don't want to use Docker, you can run the NextJS development server with hot-reloading.
First, you need to create a .env or .env.development file in the apps/web directory containing the environment variables (you can use the .env.development.example file as a template).
Please follow the file name convention, because it is used by Nextjs to load them automatically.
Install the dependencies and run the development server:
pnpm install
pnpm devThis will start the NextJS production server.
First, you need to create a .env or .env.production file in the apps/web directory containing the environment variables (you can use the .env.production.example file as a template).
Please follow the file name convention, because it is used by Nextjs to load them automatically.
Install the dependencies and run the production server:
pnpm install
pnpm build
pnpm startThis will build the NextJS standalone application and run the generated Node server.
First, you need to create a .env or .env.production file in the apps/web directory containing the environment variables (you can use the .env.production.example file as a template).
Please follow the file name convention, because it is used by Nextjs to load them automatically.
Install the dependencies and start the server:
pnpm install
pnpm build
pnpm start:standalone| Package | Version | Description |
|---|---|---|
| @emotion/cache | ^11.14.0 | Emotion's cache for CSS-in-JS |
| @emotion/react | ^11.14.0 | CSS-in-JS library for React |
| @emotion/styled | ^11.14.0 | Styled components for Emotion |
| @fontsource/roboto | ^5.1.0 | Self-hosted Roboto font files |
| @googlemaps/markerclusterer | ^2.5.3 | Marker clustering for Google Maps |
| @mui/icons-material | ^6.3.0 | Material UI icons library |
| @mui/material | ^6.3.0 | Material UI component library |
| @mui/material-nextjs | ^6.3.0 | Material UI integration for Next.js |
| @vis.gl/react-google-maps | ^1.4.2 | React components for Google Maps |
| file-saver | ^2.0.5 | File saving functionality for browsers |
| fuse.js | ^7.0.0 | Lightweight fuzzy-search library |
| i18next | ^24.2.0 | Internationalization framework |
| i18next-resources-to-backend | ^1.2.1 | i18next backend for resource files |
| lodash | ^4.17.21 | JavaScript utility library |
| next | 15.1.2 | React framework for production |
| next-i18n-router | ^5.5.1 | i18n routing for Next.js |
| react | ^19.0.0 | JavaScript library for building user interfaces |
| react-dom | ^19.0.0 | React package for DOM rendering |
| react-dropzone | ^14.3.5 | Drag and drop file upload for React |
| react-i18next | ^15.4.0 | i18next integration for React |
| react-markdown | ^10.0.0 | Markdown renderer for React |
| xlsx | ^0.18.5 | Excel file parser and generator |
| Package | Version | Description |
|---|---|---|
| @eslint/eslintrc | ^3 | ESLint configuration utility |
| @types/file-saver | ^2.0.7 | TypeScript definitions for file-saver |
| @types/lodash | ^4.17.13 | TypeScript definitions for lodash |
| @types/node | ^20 | TypeScript definitions for Node.js |
| @types/react | ^19 | TypeScript definitions for React |
| @types/react-dom | ^19 | TypeScript definitions for React DOM |
| eslint | ^9 | JavaScript linting utility |
| eslint-config-next | 15.1.2 | ESLint configuration for Next.js |
| typescript | ^5 | JavaScript with syntax for types |
The application supports multiple languages using i18next. Currently supported languages:
- English (en)
- Spanish (es)
src/locales/
├── en/
│ ├── common.json
│ ├── deforestationAnalysis.json
│ ├── home.json
│ └── polygonValidation.json
└── es/
├── common.json
├── deforestationAnalysis.json
├── home.json
└── polygonValidation.json
Each JSON file corresponds to a specific page or feature:
common.json: Shared translations used across the applicationdeforestationAnalysis.json: Translations for the deforestation analysis modulehome.json: Home page translationspolygonValidation.json: Translations for polygon validation module
If you want to add a new language, please follow the instructions in the New Language Documentation file.
/documentation (the book button in the header) explains to the app's users how farms are validated, how their area is measured and how deforestation is calculated. Its text is Markdown, one file per section and language, in src/content/documentation/<locale>/ (validation.md, area.md, deforestation.md). The page reads them on the server and renders them with react-markdown. Each file is one section of the page, and its name is the section's anchor (/documentation#area).
- Thresholds. The text never states the thresholds: it writes
{{overlapThreshold}}and{{deforestationThreshold}}, which the page replaces with the values loaded fromGET /config(src/utils/documentation.ts). A test checks that both languages have the same sections and use the same known placeholders. - Keeping it in step. The technical explanations are in
docs/(deforestation_calculation.md,polygon_area_calculation.md) and in the API's README. When a calculation or a validation changes, update the page's Markdown in both languages along with them. - A new language needs its own folder under
src/content/documentation/.
- We use ESLint and Prettier for code formatting
- Component naming follows PascalCase (e.g.,
MapComponent.tsx) - Hooks use camelCase with 'use' prefix (e.g.,
useMapData.ts) - CSS-in-JS follows BEM-like naming conventions
The types of every API request and response are generated from the API's OpenAPI
(apps/api/openapi.json) into src/api/schema.d.ts with openapi-typescript. Don't
edit that file, and don't write API shapes by hand: src/interfaces/ re-exports the
generated types under the names the code uses (FarmData, MapData, …) and only
declares types that exist just in the frontend.
When the API changes a model, run pnpm contracts at the repository root (it
regenerates both files), fix what tsc --noEmit reports, and commit both generated
files. pnpm generate:api-types regenerates only the types, from the committed
openapi.json.
Unit tests run with Vitest in a jsdom environment, with Testing Library for hooks:
pnpm test # the whole suite, once
pnpm test:watch # re-runs the affected tests on savepnpm test at the repository root runs the API's pytest suite and then this one.
-
Where they live: next to the code, as
src/**/*.test.ts(x). Shared helpers are insrc/test/:- the setup;
- typed factories for farms, layers and results (
factories.ts); renderHookWithData(renderWithData.tsx), which renders a hook under an injectedDataContextwith the real translations. Itswrapperoption mounts a provider inside that context, as theReportProvidertests do;renderWithDataProvider, which mounts the realDataProvider;deferred, to settle a mocked response when the test chooses;FakeWorker/installFakeWorkerandstubObjectUrls, for the browser APIs jsdom lacks;- the upload-file helpers (
files.ts).
-
What they cover:
- the formatting and domain helpers in
src/utils/; - upload validation and the real upload templates in
public/files/; - en/es translation parity;
- the hooks that read
DataContext; - the context providers (
DataProvider,AdminSessionProvider,ReportProvider); - the PDF worker client.
The PDF rendering itself (
reportPdf.worker.tsx) and the components aren't covered. - the formatting and domain helpers in
-
What is replaced (
src/test/setup.ts):next/navigation(a shared router whose methods are spies;setSearchParamsandsetParamscontrol what the hooks read),file-saver, andfetch. A request a test didn't stub fails that test; a test that needs the API mocks the@/api/*module it goes through. Translations are the real files, not a mock. -
Module state: after every test, the setup resets:
- the runtime config (
GET /config). A test that formats thresholds sets its own withsetRuntimeConfig; DataContext's kept flow and selected country (resetDataStoreForTests);- both storages;
- the timers.
- the runtime config (
-
Fake timers: a test that needs them (the layer polling, the admin session expiry) calls
vi.useFakeTimersitself, and advances time withvi.advanceTimersByTimeAsyncinsideact. i18next needs real timers to start, so such a test awaitscreateTestI18nfor its locales before faking them; the instance is then reused. -
Imports: tests import
describe,it,expectandvifromvitest(no globals), andtsc --noEmittype-checks them with the rest of the app.
Pull requests into dev and main marked "ready for review" are validated by the
Tests, type-check, lint, build job of the CI GitHub Actions workflow
(.github/workflows/ci.yml), which runs pnpm install --frozen-lockfile,
a check that src/api/schema.d.ts matches apps/api/openapi.json, tsc --noEmit,
pnpm run lint, pnpm test, and pnpm run build on Node 24, caching the pnpm store and
.next/cache. It runs when the PR changes apps/web/ or the API contract
(apps/api/openapi.json), or the workflow itself; otherwise it is skipped, which counts
as passed. Draft PRs are skipped.
- Image optimization using Next.js Image component
- Code splitting and lazy loading for routes
- Server-side rendering (SSR) for initial page loads
- Static site generation (SSG) for static pages
- Chrome (latest 2 versions)
- Firefox (latest 2 versions)
- Safari (latest 2 versions)
- Edge (latest 2 versions)
There is no caching strategy implemented because the analysis are executed on-demand and the results are not stored.
- Server State: React Query for API data
- UI State: React's useState and useReducer
- Global State: React Context for:
- Theme preferences
- User settings
- Authentication
- Language preferences
- Create a new branch from
dev - Make your changes
- Submit a pull request against
dev - Wait for review and approval
-
Build failures
# Clear Next.js cache rm -rf .next pnpm build -
API connection issues
- Verify NEXT_PUBLIC_API_URL in .env
- Check if backend is running
- Confirm CORS settings
pnpm dev # Run the development server
pnpm build # Build the production application
pnpm start # Start the production server
pnpm lint # Run ESLint
pnpm docker:build # Build the docker image
pnpm generate:api-types # Regenerate src/api/schema.d.ts from apps/api/openapi.jsonNEXT_PUBLIC_GET_MAPS_URL= # URL to get available maps for deforestation analysis
NEXT_PUBLIC_GET_CONFIG_URL= # URL of the API's product settings (GET /config); defaults to NEXT_PUBLIC_API_URL/config
NEXT_PUBLIC_POLYGON_VALIDATION_PARSER_URL= # URL to parse excel file data into valid Farm objects for polygon validation module
NEXT_PUBLIC_POLYGON_VALIDATION_URL= # URL to execute polygons validation and find inconsistencies
NEXT_PUBLIC_DEFORESTATION_ANALYSIS_PARSER_URL= # URL to parse excel file data into valid Farm objects for deforestation analysis
NEXT_PUBLIC_DEFORESTATION_ANALYSIS_URL= # URL to execute deforestation analysis
NEXT_PUBLIC_DEFORESTATION_ANALYSIS_TILES_URL= # URL to get map tiles with deforestation data drawn on them
NEXT_PUBLIC_GCP_MAPS_PLATFORM_API_KEY= # Google Maps API key
NEXT_PUBLIC_MAX_REQUESTS_FOR_SATELLITE_BACKGROUND_AT_DEFORESTATION_IMAGE_GENERATION= # Most farms whose report images get a satellite background (one Google Static Maps call per farm); above it they get a solid one. Empty = no limitThe overlap and deforestation thresholds are not frontend variables: the API owns them (OVERLAP_THRESHOLD_PERCENTAGE, DEFORESTATION_THRESHOLD_PERCENTAGE) and publishes them at GET /config. The app loads them once at startup (src/config/runtime.ts) and renders the pages only after they arrive; if they can't be loaded it shows an error with a retry button.
The report is rendered in the browser with @react-pdf/renderer, off the main thread:
ReportProvider(src/context/ReportContext.tsx) wraps the preview page. It fetches the report images once per selection (POST /deforestation_analysis/generate-image, JPEG) and shares them with the preview and both downloads.- Every PDF renders in a Web Worker (
src/workers/reportPdf.worker.tsx), which sets up its own i18next and runtime config. The page only shows the result in an iframe. - Once the preview is shown, the complete report (with links, which the preview hides) is pre-rendered, so "Download" saves it at once. The separated reports (a ZIP) render on click.
- Large reports are those with more than
LARGE_REPORT_FARM_PAGES(1000, insrc/config/constants.ts) farm pages, that is, selected (farm, layer) pairs with a result. A complete PDF of a few thousand pages runs the tab out of memory, so:- the preview shows only the first farm's PDF, with a notice;
- there is no complete PDF and no pre-render;
- "Download ZIP" builds a ZIP with one PDF per farm, farm by farm. Each farm's images are fetched a few farms ahead (with retries), its PDF is added in the worker, and its images are released, so memory doesn't grow with the farms. The footer shows the progress and can cancel the job, and leaving the page cancels it too. 1000 farms × 5 layers take about 3 minutes and give ~245 MB.
- The fonts (
public/fonts/roboto: Roboto regular, medium, bold, italic and bold italic, Apache 2.0) and images are served by the app itself: rendering makes no third-party requests.
The endpoints of each module are defined as environment variables because this project is modularized and each module has its own backend service. You could use your own backend services by changing the environment variables and following the same structure for the requests and responses.
- Material UI for consistent design system
- Emotion for CSS-in-JS styling
- React Query for server state management
- i18next for internationalization
- Google Maps for mapping functionality
Frontend dependencies are updated by Dependabot (npm ecosystem on /apps/web,
plus a docker entry for the two Dockerfiles), weekly, with minor and patch updates
grouped into one pull request and each major isolated in its own. Nothing is
automerged. See the root README for the full policy.
Two dependencies need a manual check, because Dependabot can't update them:
xlsx(SheetJS) comes from SheetJS's CDN, not npm: SheetJS stopped publishing to npm at 0.18.5, which has known vulnerabilities (prototype pollution, ReDoS), and publishes its fixes only atcdn.sheetjs.com.package.jsonpins the versioned tarball (https://cdn.sheetjs.com/xlsx-<version>/xlsx-<version>.tgz).- To bump it: check the release notes at https://docs.sheetjs.com (or
https://cdn.sheetjs.com/ for the versions), run
pnpm add "xlsx@https://cdn.sheetjs.com/xlsx-<version>/xlsx-<version>.tgz", then uploadapps/api/tests/regression/regression_farms.xlsxin both modules and download both Excel results. - Its content isn't verified. pnpm records no integrity hash for a remote
tarball, so the lockfile pins the URL, not the bytes: we trust SheetJS's HTTPS
host. If that ever stops being acceptable, vendor the tarball
(
apps/web/vendor/xlsx-<version>.tgz, declared asfile:vendor/..., and copied into both Dockerfiles beforepnpm install): pnpm then records itssha512and--frozen-lockfilefails if it changes.
- To bump it: check the release notes at https://docs.sheetjs.com (or
https://cdn.sheetjs.com/ for the versions), run
pnpm.overridesinpackage.json:"exceljs>uuid": "^11.1.1".exceljs4.4.0 (its latest release) asks foruuid ^8.3.0, and the fix for GHSA-w5hq-g745-h8pq is in 11.1.1.exceljsonly callsv4(), whichuuid11 keeps, and the browser loadsexceljs's prebundled build anyway. Remove it whenexceljsallowsuuid >= 11.1.1.
Other transitive alerts are fixed by refreshing the lockfile within the parents'
ranges (pnpm update <package> --depth Infinity), not by overrides.
TODO