| name | frontend |
| description | Use when working on app-partners (partner dashboard) or app-observatory (public statistics). Covers Next.js 15 patterns, DSFR components, API hooks, maps (MapLibre/Deck.gl), charts, and Meilisearch. |
Frontend Developer Skill
Read app-partners/README.md or app-observatory/README.md first for setup and configuration.
App Partners — Key Patterns
API Integration
Custom useRestQuery hook:
const { data, loading, error, refetch } = useRestQuery<DataType>(
"v3",
"endpoint/path",
params,
{ method: "POST", paginate: false },
[dependencies]
);
Available hooks:
useCampaignList(), useCampaignFind()
useUsersList(), useOperatorsList(), useTerritoriesList()
useExportsList(), useExportCreate(), useExportDownloadLink()
useIncentiveGraph(), useOperatorsGraph()
State Management
React Context for authentication, no Redux/Zustand:
const { isAuth, user, logout } = useAuth();
DSFR Components
import { Button, Input, Select, Table, Alert } from "@codegouvfr/react-dsfr";
import { fr } from "@codegouvfr/react-dsfr";
className={fr.cx("fr-container", "fr-mb-7w")}
Key components: Header, Footer, Button, Input, Select, Table, Pagination, Alert, Badge, Download.
Form Validation (Zod)
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
Input error state:
<Input
state={errors.email ? "error" : "default"}
stateRelatedMessage={errors.email}
/>
Modal Forms
useActionsModal hook for CRUD operations:
const modal = useActionsModal<DataType>();
Authentication
ProConnect SSO flow:
- Login via
ProConnectButton -> ProConnect URL
- Session check on load:
GET /auth/me
- Role-based access control
Role helpers:
isRegistry(), isTerritory(), isOperator(), isAdmin(), isUser()
Analytics
import { sendEvent } from "@socialgouv/matomo-next";
void sendEvent({ category: "campagne", action: "Consultation" });
Conventions
- Components: PascalCase (
AuthButton.tsx)
- Hooks: camelCase with
use prefix (useCampaignList.ts)
- Interfaces: PascalCase with
Interface suffix
- Client components: Add
"use client" directive
- Pre-build hook:
react-dsfr update-icons
App Observatory — Key Patterns
Map Integration
import Map from "react-map-gl/maplibre";
import DeckGL from "@deck.gl/react";
Deck.gl layers:
H3HexagonLayer - Density visualization with hexagonal bins
ArcLayer - Origin-destination flow visualization
H3 Hexagonal Binning
import { cellToBoundary, cellToLatLng } from "h3-js";
const h3Index = latLngToCell(lat, lng, 9);
Data Visualization
Chart.js with react-chartjs-2:
import { Line } from "react-chartjs-2";
import { Chart, CategoryScale, LinearScale, PointElement, LineElement } from "chart.js";
Chart.register(CategoryScale, LinearScale, PointElement, LineElement);
API Data Fetching
const { data, loading, error } = useApi<DataType>(url, params);
const jsonData = useJson<DataType>(url);
Key endpoints:
| Endpoint | Purpose |
|---|
/observatory/journeys | Journey statistics |
/observatory/directions | Direction analysis |
/observatory/od | Origin-destination matrices |
/observatory/territories | Territory data |
Meilisearch Integration
import { MeiliSearch } from "meilisearch";
const client = new MeiliSearch({ host: MEILISEARCH_URL });
const results = await client.multiSearch({
queries: [
{ indexUid: "article", q: query },
{ indexUid: "resource", q: query },
{ indexUid: "page", q: query },
],
});
MDX Content
import { MDXRemote } from "next-mdx-remote/rsc";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
<MDXRemote
source={content}
options={{ mdxOptions: { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSlug] }}}
/>
Content fetched from Strapi CMS with 60s revalidation.
Geographic Data
type PerimeterType = "country" | "region" | "department" | "aom" | "epci" | "commune";
type INSEECode = string;
Jenks natural breaks for choropleth maps:
import { jenks } from "simple-statistics";
const breaks = jenks(values, 5);
Conventions
- Suspense boundaries for async data
- Server/client component split
- Container/presentational pattern
- Strong TypeScript typing for geographic data
- Pre-build hook:
only-include-used-icons