diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 000000000..ecf4237ff --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,41 @@ +name: Quality (lint + typecheck) + +# Garde-fous statiques du frontend. Introduits en amont de l'upgrade Next 16 +# pour rendre visibles les regressions de typage et de lint. +# +# Etat actuel : NON BLOQUANT. Le depot demarre avec un passif d'erreurs de +# typage et de lint ; les etapes sont donc en `continue-on-error` le temps de +# resorber ce passif. Le passage en bloquant est le sujet de TASK-12. + +on: + push: + branches: + - main + pull_request: + +jobs: + frontend-quality: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile --filter frontend + + - name: Typecheck + continue-on-error: true + run: pnpm --filter frontend run typecheck + + - name: Lint + continue-on-error: true + run: pnpm --filter frontend run lint diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index ef7844df1..4645563c7 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -76,10 +76,14 @@ COPY --from=builder /app/frontend/.next/static /app/frontend/.next/static COPY --from=builder /app/frontend/public /app/frontend/public COPY --from=builder /app/frontend/messages /app/frontend/messages -# Copy the server wrapper that ensures env vars are available -COPY frontend/server-wrapper.js /app/frontend/server-wrapper.js +# Note : aucun wrapper Node n'est necessaire. Le serveur standalone de Next lit +# les variables d'environnement au moment de la requete (seules les variables +# NEXT_PUBLIC_* sont figees a la compilation). Le fichier server-wrapper.js qui +# se contentait de re-affecter process.env.X = process.env.X a ete supprime : +# ces auto-affectations n'avaient aucun effet. Verifie en executant directement +# `node frontend/server.js` avec les variables fournies au runtime. EXPOSE 3000 ENV PORT=3000 -CMD ["node", "frontend/server-wrapper.js"] +CMD ["node", "frontend/server.js"] diff --git a/eslint.config.mjs b/eslint.config.mjs index f87e22dad..b2d34632e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,105 +1,103 @@ -// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format -import tsParser from '@typescript-eslint/parser'; -import tsPlugin from '@typescript-eslint/eslint-plugin'; +import antfu from '@antfu/eslint-config'; +import nextPlugin from '@next/eslint-plugin-next'; +import jsxA11y from 'eslint-plugin-jsx-a11y'; +import storybook from 'eslint-plugin-storybook'; -export default [ +/** + * Configuration ESLint du monorepo (flat config, ESLint 9). + * + * Base : @antfu/eslint-config (config de reference de l'equipe, cf. decision-1). + * Par-dessus : regles specifiques Next.js, accessibilite (jsx-a11y) et Storybook, + * qui etaient absentes alors que les plugins etaient deja en dependance. + */ +export default antfu( { + type: 'app', + react: true, + typescript: true, + // Le depot n'a jamais eu de formateur et son style est heterogene : les + // regles de forme (style/*) et de tri (perfectionist) produiraient environ + // 2900 erreurs de formatage, qui noieraient le signal utile. Le formatage + // est un sujet distinct, a traiter avec son propre outil et son propre diff. + stylistic: false, + jsonc: false, + yaml: false, + markdown: false, + toml: false, ignores: [ - 'migrations/**/*', - 'next-env.d.ts', - '**/contentTypes.d.ts', - '**/storybook-static/**/*', - '.next/**/*', - '**/.next/**/*', - '**/.next/types/**/*', - '**/.next/server/**/*', - '**/.next/static/**/*', - '**/.next/trace/**/*', - '**/.next/cache/**/*', - '**/.next/standalone/**/*', - '**/.next/swc/**/*', - '**/.next/webpack/**/*', - '**/.next/on-demand-entries/**/*', - '**/.next/prerender-manifest.json', - '**/.next/routes-manifest.json', - '**/.next/build-manifest.json', - '**/.next/required-server-files-manifest.json', - '**/.next/static/chunks/**/*', - '**/.next/static/css/**/*', - '**/.next/static/media/**/*', - '**/.next/static/webpack/**/*', - 'node_modules/**/*', - '**/dist/**/*', - '**/build/**/*', - '**/coverage/**/*', - '**/.turbo/**/*', - '**/storybook-static/**/*', - '**/.storybook/**/*', - '**/public/**/*', - '**/backend/**/*', - '**/docker/**/*', - '**/migrations/**/*', - '**/*.min.js', - '**/*.bundle.js', - '**/vendor/**/*', - '**/chunks/**/*', - '**/webpack-runtime.js', - '**/polyfills.js', - '**/fallback/**/*', - '**/vendor-chunks/**/*', - '**/types/**/*.d.ts', - '**/generated/**/*', + // Le backend Strapi a ses propres regles. + 'backend/**', + 'docker/**', + 'migrations/**', + '**/.next/**', + '**/storybook-static/**', + '**/.storybook/**', + '**/coverage/**', + // Types generes, jamais edites a la main. + '**/*.d.ts', + '**/generated/**', + '**/strapi-types.d.ts', '**/auto-imports.d.ts', '**/components.d.ts', - '**/nuxt.d.ts', - '**/imports.d.ts', - '**/composables.d.ts', - '**/utils.d.ts' ], }, { - files: ['**/*.{js,jsx,ts,tsx}'], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, + // Regles specifiques a l'application Next : elles ne visent que le frontend. + files: ['frontend/**/*.{js,jsx,ts,tsx}'], + plugins: { + '@next/next': nextPlugin, + 'jsx-a11y': jsxA11y, + }, + settings: { + // Sans cela, le plugin Next cherche un dossier `pages/` a la racine du + // monorepo et avertit a chaque execution. + next: { rootDir: 'frontend' }, }, rules: { - // Basic rules - 'no-unused-vars': 'warn', - 'no-console': 'warn', - 'prefer-const': 'error', - 'no-var': 'error', - // Indentation rules - 'indent': ['error', 2], - 'no-mixed-spaces-and-tabs': 'error', + ...nextPlugin.configs.recommended.rules, + ...nextPlugin.configs['core-web-vitals'].rules, + ...jsxA11y.flatConfigs.recommended.rules, }, }, { - files: ['**/*.{ts,tsx}'], - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, + // Le tri des imports/exports est purement cosmetique : il produirait plus + // de 140 erreurs sans rapport avec la correction du code. A traiter avec le + // formateur, dans un diff dedie. + rules: { + 'perfectionist/sort-imports': 'off', + 'perfectionist/sort-named-imports': 'off', + 'perfectionist/sort-exports': 'off', + 'perfectionist/sort-named-exports': 'off', + 'import/consistent-type-specifier-style': 'off', }, - plugins: { - '@typescript-eslint': tsPlugin, + }, + { + // Regles utiles mais dont le volume depasse l'urgence : signalees sans + // bloquer, en attendant une passe de nettoyage dediee. + rules: { + 'unused-imports/no-unused-imports': 'warn', + 'unused-imports/no-unused-vars': 'warn', + 'react/no-array-index-key': 'warn', + 'react-dom/no-dangerously-set-innerhtml': 'warn', }, + }, + { + // Ces regles ont besoin de l'information de type : elles ne peuvent viser + // que les fichiers TypeScript (sinon `middleware.js` fait echouer ESLint). + files: ['**/*.{ts,tsx}'], + rules: { + 'ts/consistent-type-imports': 'warn', + 'ts/consistent-type-definitions': 'warn', + }, + }, + { + // Les stories sont des fixtures : elles ont le droit d'etre verbeuses et de + // manipuler des donnees incompletes. + files: ['**/*.stories.@(ts|tsx|js|jsx)', '**/*.story.@(ts|tsx|js|jsx)'], rules: { - '@typescript-eslint/no-unused-vars': 'warn', - '@typescript-eslint/no-var-requires': 'error', - // TypeScript indentation rules - '@typescript-eslint/indent': ['error', 2], - 'no-mixed-spaces-and-tabs': 'error', + 'react-hooks/rules-of-hooks': 'off', + 'no-console': 'off', }, }, -]; + ...storybook.configs['flat/recommended'], +); diff --git a/frontend/.gitignore b/frontend/.gitignore index ffc9fe25f..6a43a7383 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -38,3 +38,8 @@ next-env.d.ts *storybook.log storybook-static + +# fichiers generes automatiquement par Next 16 a chaque "next dev" +# (desactivable avec agentRules: false dans next.config.mjs) +AGENTS.md +CLAUDE.md diff --git a/frontend/.storybook/vitest.setup.ts b/frontend/.storybook/vitest.setup.ts index c5ed05fa0..f117c7b2e 100644 --- a/frontend/.storybook/vitest.setup.ts +++ b/frontend/.storybook/vitest.setup.ts @@ -1,7 +1,15 @@ -import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; +import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; import { setProjectAnnotations } from '@storybook/nextjs-vite'; +import nextIntlAnnotations from 'storybook-next-intl/preview'; import * as projectAnnotations from './preview'; -// This is an important step to apply the right configuration when testing your stories. -// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations -setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); \ No newline at end of file +// Ce fichier est indispensable : sans lui, les tests echouent sur +// « NoRenderFunctionError: No render function available », car les annotations +// de preview ne sont pas appliquees automatiquement dans cette configuration. +// +// `storybook-next-intl` doit y etre liste EXPLICITEMENT : `setProjectAnnotations` +// remplace les annotations fournies par les addons au lieu de les completer. +// Sans cette ligne, 67 tests echouent sur +// « Failed to call `useTranslations` because the context from +// `NextIntlClientProvider` was not found ». +setProjectAnnotations([a11yAddonAnnotations, nextIntlAnnotations, projectAnnotations]); diff --git a/frontend/README.md b/frontend/README.md index f56a50475..0feb023df 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -36,7 +36,31 @@ pnpm run storybook # Construire Storybook pour la production pnpm run build-storybook + +# Lancer les tests de composants (Vitest + navigateur, voir plus bas) +pnpm run test +pnpm run test:watch +``` + +#### Tests de composants + +Les stories servent de tests : chaque story est rendue dans un vrai navigateur (Vitest + +Playwright) et vérifiée. Lancer `pnpm run test` depuis `frontend/`. + +Si l'exécution échoue avec un message du type : + ``` +Error: browserType.launch: Host system is missing dependencies to run browsers. +``` + +c'est que le Chromium embarqué par Playwright ne trouve pas ses bibliothèques système (fréquent +sur NixOS, ou dans une image de CI minimale). Dans ce cas, pointer vers un Chromium déjà installé : + +```bash +PLAYWRIGHT_EXECUTABLE_PATH=/chemin/vers/chromium pnpm run test +``` + +Ce chemin est lu par `vitest.config.ts` et transmis à Playwright. #### Structure des composants diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 367fd0d03..a01161f01 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -4,34 +4,59 @@ import createNextIntlPlugin from 'next-intl/plugin'; const nextConfig = { output: 'standalone', images: { - domains: [ - 'localhost', - 'backend', - 'dataforgood.fr', - 'strapi.services.dataforgood.fr', - 's3.fr-par.scw.cloud', - 'images.pexels.com', + // `domains` est deprecie : remplace par `remotePatterns` (equivalent exact). + remotePatterns: [ + { protocol: 'http', hostname: 'localhost' }, + { protocol: 'http', hostname: 'backend' }, + { protocol: 'https', hostname: 'dataforgood.fr' }, + { protocol: 'https', hostname: 'strapi.services.dataforgood.fr' }, + { protocol: 'https', hostname: 's3.fr-par.scw.cloud' }, + { protocol: 'https', hostname: 'images.pexels.com' }, ], }, redirects: () => getRedirects(), - // VRAIMENT PAS OUF - eslint: { - ignoreDuringBuilds: true, - }, typescript: { ignoreBuildErrors: true, }, }; export async function getRedirects() { + const apiUrl = process.env.STRAPI_API_URL; + const token = process.env.STRAPI_API_TOKEN; + + // Sans jeton, /redirects repond 403 avec un corps d'erreur JSON. L'ancien code + // appelait data.map() sur ce corps et levait « data.map is not a function », + // ce qui masquait la vraie cause (absence de jeton) derriere un TypeError. + if (!apiUrl || !token) { + console.warn( + '[redirects] STRAPI_API_URL ou STRAPI_API_TOKEN absent : aucune redirection du CMS ne sera chargee.', + ); + return []; + } + try { - const res = await fetch(`${process.env.STRAPI_API_URL}/redirects`, { + const res = await fetch(`${apiUrl}/redirects`, { headers: { - Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`, + Authorization: `Bearer ${token}`, }, }); + + if (!res.ok) { + console.warn( + `[redirects] /redirects a repondu ${res.status} ${res.statusText} : aucune redirection du CMS ne sera chargee.`, + ); + return []; + } + const data = await res.json(); + if (!Array.isArray(data)) { + console.warn( + '[redirects] reponse inattendue de /redirects (un tableau etait attendu) : aucune redirection du CMS ne sera chargee.', + ); + return []; + } + return data.map((redirect) => ({ source: redirect.source, destination: redirect.destination, diff --git a/frontend/package.json b/frontend/package.json index 4bdddc9d1..65fe5923c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,73 +6,60 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", - "lint:fix": "eslint --fix", - "generate:types": "openapi-typescript ../backend/src/extensions/documentation/documentation/1.0.0/full_documentation.json -o src/lib/strapi-types.d.ts", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "generate:types": "openapi-typescript ../backend/src/extensions/documentation/documentation/1.0.0/full_documentation.json -o src/lib/strapi-types.d.ts && node ./scripts/widen-strapi-query-types.mjs", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "test": "vitest run --project storybook", + "test:watch": "vitest --project storybook", + "typecheck": "tsc --noEmit" }, "dependencies": { "@radix-ui/react-accordion": "^1.2.10", "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-navigation-menu": "^1.2.1", "@radix-ui/react-slot": "^1.2.2", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tooltip": "^1.1.8", - "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "embla-carousel-react": "^8.6.0", "lucide-react": "^0.447.0", - "mdast-util-toc": "^7.1.0", "motion": "^12.23.13", - "next": "14.2.14", - "next-intl": "^4.3.4", - "next-video": "^2.5.0", + "next": "16.3.5", + "next-intl": "^4.14.5", "openapi-fetch": "^0.14.0", "openapi-typescript": "^7.8.0", "qs": "^6.14.0", - "react": "^18", - "react-confetti": "^6.4.0", - "react-dom": "^18", + "react": "19.3.0", + "react-dom": "19.3.0", "react-icons": "^5.5.0", - "rehype-autolink-headings": "^7.1.0", - "rehype-raw": "^7.0.0", - "rehype-slug": "^6.0.0", - "rehype-stringify": "^10.0.1", "remark": "^15.0.1", "remark-html": "^16.0.1", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.1", "sanitize-html": "^2.13.1", - "tailwind-merge": "^2.5.2", - "tailwindcss-animate": "^1.0.7", - "unified": "^11.0.5" + "tailwind-merge": "^2.5.2" }, "devDependencies": { - "@chromatic-com/storybook": "^4.0.1", - "@storybook/addon-a11y": "^9.0.18", - "@storybook/addon-docs": "^9.0.18", - "@storybook/addon-onboarding": "^9.0.18", - "@storybook/addon-vitest": "^9.0.18", - "@storybook/nextjs-vite": "^9.0.18", - "@strapi/strapi": "5.0.2", + "@chromatic-com/storybook": "^5.3.1", + "@storybook/addon-a11y": "10.6.0", + "@storybook/addon-docs": "10.6.0", + "@storybook/addon-onboarding": "10.6.0", + "@storybook/addon-vitest": "10.6.0", + "@storybook/nextjs-vite": "10.6.0", "@tailwindcss/typography": "^0.5.15", "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@types/sanitize-html": "^2.16.0", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.44.0", - "@vitest/browser": "^3.2.4", - "@vitest/coverage-v8": "^3.2.4", - "eslint-plugin-storybook": "^9.0.18", + "@vitest/browser": "^4.1.11", + "@vitest/browser-playwright": "^4.1.11", + "@vitest/coverage-v8": "^4.1.11", + "eslint-plugin-storybook": "^10.6.0", "playwright": "^1.54.1", "postcss": "^8", - "storybook": "^9.0.18", - "storybook-next-intl": "^2.0.13", + "storybook": "10.6.0", + "storybook-next-intl": "^10.1.4", "tailwindcss": "^3.4.1", "typescript": "^5", - "vitest": "^3.2.4" + "vitest": "^4.1.11" } } diff --git a/frontend/scripts/widen-strapi-query-types.mjs b/frontend/scripts/widen-strapi-query-types.mjs new file mode 100644 index 000000000..f7d99dbc8 --- /dev/null +++ b/frontend/scripts/widen-strapi-query-types.mjs @@ -0,0 +1,41 @@ +/** + * Ajuste les types de requete generes par openapi-typescript a partir de la + * documentation Strapi. + * + * Le plugin OpenAPI de Strapi declare `populate` comme une simple chaine de + * caracteres. L'API accepte en realite la syntaxe imbriquee utilisee partout + * dans le frontend (`populate: { thematic: { populate: { projects: '*' } } }`), + * que les types generes rejettent. On elargit donc localement, apres generation. + * + * Ce fichier est execute automatiquement par `pnpm generate:types` : la + * correction survit a toute regeneration. + */ +import fs from 'node:fs'; +import path from 'node:path'; + +const target = path.resolve(import.meta.dirname, '../src/lib/strapi-types.d.ts'); +const source = fs.readFileSync(target, 'utf8'); + +const replacements = [ + [/^(\s*)populate\?: string;$/gm, '$1populate?: string | Record;'], + // `fields` est lui aussi passe sous forme de tableau (`fields: ["url"]`). + [/^(\s*)fields\?: string;$/gm, '$1fields?: string | string[];'], +]; + +let output = source; +let total = 0; +for (const [pattern, replacement] of replacements) { + const before = output; + output = output.replace(pattern, replacement); + if (before !== output) { + total += (before.match(pattern) ?? []).length; + } +} + +if (output === source) { + console.log('widen-strapi-query-types: rien a elargir (types deja a jour).'); +} +else { + fs.writeFileSync(target, output); + console.log(`widen-strapi-query-types: ${total} declaration(s) elargie(s).`); +} diff --git a/frontend/server-wrapper.js b/frontend/server-wrapper.js deleted file mode 100644 index b5815f1e1..000000000 --- a/frontend/server-wrapper.js +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node - -// This wrapper ensures environment variables are properly passed to the Next.js standalone server -// The standalone server doesn't automatically pick up runtime environment variables - -// Make sure all environment variables are available -process.env.BREVO_API_KEY = process.env.BREVO_API_KEY; -process.env.STRAPI_API_TOKEN = process.env.STRAPI_API_TOKEN; -process.env.STRAPI_API_URL = process.env.STRAPI_API_URL; - -// Now start the actual Next.js server -require('./server.js'); \ No newline at end of file diff --git a/frontend/src/app/[locale]/about/about.tsx b/frontend/src/app/[locale]/about/about.tsx index 5fc2d114e..a5dff9c74 100644 --- a/frontend/src/app/[locale]/about/about.tsx +++ b/frontend/src/app/[locale]/about/about.tsx @@ -14,10 +14,16 @@ import { import { IMembers } from '@/lib/types'; import { AboutPageData } from './page'; -function transformTestimonials( - testimonials: NonNullable -) { - return testimonials.map(testimonial => ({ +// Les relations du CMS peuvent etre absentes de la reponse (relation non +// renseignee, ou droits d'acces differents selon le jeton utilise). Les +// transformations tolerent donc l'absence et retombent sur un tableau vide : +// sans cela la page entiere plantait au rendu (`Cannot read properties of +// undefined (reading 'map')`). + +type AboutMember = NonNullable[number]; + +function transformTestimonials(testimonials: AboutPageData['testimonials']) { + return (testimonials ?? []).map(testimonial => ({ id: testimonial.id, author: testimonial.author, content: testimonial.quote, @@ -25,8 +31,8 @@ function transformTestimonials( })); } -function transformFunders(funders: NonNullable) { - return funders.map(funder => ({ +function transformFunders(funders: AboutPageData['funders']) { + return (funders ?? []).map(funder => ({ id: funder.id, name: funder.name, description: funder.description, @@ -35,9 +41,7 @@ function transformFunders(funders: NonNullable) { })); } -function transformMember( - member: NonNullable -) { +function transformMember(member: AboutMember) { return { id: member.id, name: member.name, @@ -54,42 +58,40 @@ function transformMembers({ strategic_committee, division_managers, }: { - board_of_directors: Array>; - employees: Array>; - scientific_committee: Array< - NonNullable - >; - strategic_committee: Array>; - division_managers: Array>; + board_of_directors: AboutPageData['board_of_directors']; + employees: AboutPageData['employees']; + scientific_committee: AboutPageData['scientific_committee']; + strategic_committee: AboutPageData['strategic_committee']; + division_managers: AboutPageData['division_managers']; }) { return [ { title: "Le conseil d'administration", - members: board_of_directors.map(transformMember), + members: (board_of_directors ?? []).map(transformMember), }, { title: "L'équipe salariée", - members: employees.map(transformMember), + members: (employees ?? []).map(transformMember), }, { title: 'Le comité scientifique', - members: scientific_committee.map(transformMember), + members: (scientific_committee ?? []).map(transformMember), }, { title: 'Le comité stratégique', - members: strategic_committee.map(transformMember), + members: (strategic_committee ?? []).map(transformMember), }, { title: 'Les responsables de pôles', - members: division_managers.map(transformMember), + members: (division_managers ?? []).map(transformMember), }, ]; } function transformActivityReports( - reports: NonNullable + reports: AboutPageData['activity_reports'] ) { - return reports.map(report => ({ + return (reports ?? []).map(report => ({ id: report.documentId || report.id, text: new Date(report.date).getFullYear().toString(), link: report.file?.url || '#', @@ -103,8 +105,8 @@ type AboutProps = { export default function AboutPage({ data }: AboutProps) { const t = useTranslations('about'); - const testimonies = transformTestimonials(data.testimonials!); - const funders = transformFunders(data.funders!); + const testimonies = transformTestimonials(data.testimonials); + const funders = transformFunders(data.funders); const members: IMembers[] = transformMembers({ board_of_directors: data.board_of_directors, employees: data.employees, @@ -137,12 +139,12 @@ export default function AboutPage({ data }: AboutProps) { rotation: -7, className: 'sm:left-8', }} - image={data.cta_left?.image.url} + image={data.cta_left?.image?.url} imagePosition="left" contentClassName="relative lg:-top-24 lg:-left-12" cta={{ - text: data.cta_left?.cta.text, - link: data.cta_left?.cta.link, + text: data.cta_left?.cta?.text, + link: data.cta_left?.cta?.link, rotation: -3.7, className: 'relative sm:left-[182px] md:-top-4', }} @@ -159,12 +161,12 @@ export default function AboutPage({ data }: AboutProps) { rotation: 1.5, className: 'sm:left-6', }} - image={data.cta_right?.image.url ?? ''} + image={data.cta_right?.image?.url ?? ''} className="w-full lg:w-[770px] overflow-hidden md:overflow-visible" contentClassName="relative lg:top-24" cta={{ - text: data.cta_right?.cta.text, - link: data.cta_right?.cta.link, + text: data.cta_right?.cta?.text, + link: data.cta_right?.cta?.link, rotation: 0.5, className: 'relative sm:left-48 -top-2', }} @@ -180,9 +182,9 @@ export default function AboutPage({ data }: AboutProps) { <> @@ -195,9 +197,9 @@ export default function AboutPage({ data }: AboutProps) { diff --git a/frontend/src/app/[locale]/about/page.tsx b/frontend/src/app/[locale]/about/page.tsx index 2ca9e4d56..3d3a6d86d 100644 --- a/frontend/src/app/[locale]/about/page.tsx +++ b/frontend/src/app/[locale]/about/page.tsx @@ -2,11 +2,11 @@ import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; import AboutPage from './about'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await fetchAboutPageData(); if (!data?.data?.seo_meta) { diff --git a/frontend/src/app/[locale]/blog/[slug]/page.tsx b/frontend/src/app/[locale]/blog/[slug]/page.tsx index 73e878cd6..b014b4955 100644 --- a/frontend/src/app/[locale]/blog/[slug]/page.tsx +++ b/frontend/src/app/[locale]/blog/[slug]/page.tsx @@ -27,11 +27,11 @@ async function fetchBlogPageData(slug: string) { export type BlogPageData = NonNullable>["data"]>["data"]>["blogs"]>[0]; -export async function generateMetadata({ - params: { locale, slug }, -}: { - params: { locale: string; slug: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string; slug: string }> }, +) { + const { locale, slug } = await props.params; + const { data } = await fetchBlogPageData(slug); if (!data?.data || !data.data.length) { @@ -46,16 +46,16 @@ export async function generateMetadata({ }; } -export default async function Page({ params }: { params: { slug: string } }) { - const { slug } = params; - const { data } = await fetchBlogPageData(slug); +export default async function Page(props: { params: Promise<{ slug: string }> }) { + const { slug } = await props.params; + const { data } = await fetchBlogPageData(slug); - if (!data?.data || !data.data.length) { - return null; - } + if (!data?.data || !data.data.length) { + return null; + } - const blogPageData = data.data[0] as BlogPageData; - const contentHtml = await getMarkdownContent(blogPageData.content) + const blogPageData = data.data[0] as BlogPageData; + const contentHtml = await getMarkdownContent(blogPageData.content) return ; }; diff --git a/frontend/src/app/[locale]/blog/page.tsx b/frontend/src/app/[locale]/blog/page.tsx index d91b1e2cf..338eb218e 100644 --- a/frontend/src/app/[locale]/blog/page.tsx +++ b/frontend/src/app/[locale]/blog/page.tsx @@ -3,11 +3,11 @@ import BlogPage from './blog'; import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await client.GET('/blog-list', { params: { query: { diff --git a/frontend/src/app/[locale]/cgu/page.tsx b/frontend/src/app/[locale]/cgu/page.tsx index 525eb43c6..05d876d2c 100644 --- a/frontend/src/app/[locale]/cgu/page.tsx +++ b/frontend/src/app/[locale]/cgu/page.tsx @@ -3,11 +3,11 @@ import CguPage from './cgu'; import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await client.GET('/cgu', { params: { query: { diff --git a/frontend/src/app/[locale]/charte/page.tsx b/frontend/src/app/[locale]/charte/page.tsx index 7be809655..c2e617969 100644 --- a/frontend/src/app/[locale]/charte/page.tsx +++ b/frontend/src/app/[locale]/charte/page.tsx @@ -3,11 +3,11 @@ import ChartePage from './charte'; import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await client.GET('/diversity-charter', { params: { query: { diff --git a/frontend/src/app/[locale]/climate-and-biodiversity/page.tsx b/frontend/src/app/[locale]/climate-and-biodiversity/page.tsx index 7e0f57d6b..a153eeb8e 100644 --- a/frontend/src/app/[locale]/climate-and-biodiversity/page.tsx +++ b/frontend/src/app/[locale]/climate-and-biodiversity/page.tsx @@ -3,11 +3,11 @@ import ClimatePage from './climate'; import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await fetchThematicPageData(); if (!data?.data?.seo_meta) { diff --git a/frontend/src/app/[locale]/democracy/page.tsx b/frontend/src/app/[locale]/democracy/page.tsx index 3fbfcf7dc..189a25bed 100644 --- a/frontend/src/app/[locale]/democracy/page.tsx +++ b/frontend/src/app/[locale]/democracy/page.tsx @@ -3,11 +3,11 @@ import DemocracyPage from './democracy'; import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await fetchThematicPageData(); if (!data?.data?.seo_meta) { diff --git a/frontend/src/app/[locale]/donations/page.tsx b/frontend/src/app/[locale]/donations/page.tsx index 6d22d4c9b..458cead9e 100644 --- a/frontend/src/app/[locale]/donations/page.tsx +++ b/frontend/src/app/[locale]/donations/page.tsx @@ -4,11 +4,11 @@ import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; import { getMarkdownContent } from '@/lib/markdown'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await fetchDonationData(); if (!data?.data?.seo_meta) { diff --git a/frontend/src/app/[locale]/events/[id]/page.tsx b/frontend/src/app/[locale]/events/[id]/page.tsx index 54b48ef98..02fc4ee23 100644 --- a/frontend/src/app/[locale]/events/[id]/page.tsx +++ b/frontend/src/app/[locale]/events/[id]/page.tsx @@ -2,11 +2,11 @@ import { getTranslations } from 'next-intl/server'; import React from 'react'; import ArticlePage from './article'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const t = await getTranslations({ locale, namespace: 'blog' }); return { diff --git a/frontend/src/app/[locale]/events/page.tsx b/frontend/src/app/[locale]/events/page.tsx index 04d14c15d..b2c9a6e54 100644 --- a/frontend/src/app/[locale]/events/page.tsx +++ b/frontend/src/app/[locale]/events/page.tsx @@ -3,11 +3,11 @@ import EventsPage from './events'; import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await client.GET('/evenement', { params: { query: { @@ -44,11 +44,10 @@ export type EventsPageResponse = NonNullable; export type EventsPageMeta = NonNullable["pagination"]>; -export default async function Page({ - searchParams, -}: { - searchParams: { [key: string]: string | string[] | undefined }; -}) { +export default async function Page( + props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }, +) { + const searchParams = await props.searchParams; const page = typeof searchParams.page === 'string' ? Number(searchParams.page) : 1; const pageSize = 6; diff --git a/frontend/src/app/[locale]/faq/page.tsx b/frontend/src/app/[locale]/faq/page.tsx index c79feeb7e..760972b23 100644 --- a/frontend/src/app/[locale]/faq/page.tsx +++ b/frontend/src/app/[locale]/faq/page.tsx @@ -3,11 +3,11 @@ import FaqPage from './faq'; import client from '@/lib/strapi-client'; import { generateMetadataFromSeo } from '@/lib/utils'; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { +export async function generateMetadata( + props: { params: Promise<{ locale: string }> }, +) { + const { locale } = await props.params; + const { data } = await client.GET('/faq', { params: { query: { diff --git a/frontend/src/app/[locale]/layout.tsx b/frontend/src/app/[locale]/layout.tsx index af1916da9..52d798712 100644 --- a/frontend/src/app/[locale]/layout.tsx +++ b/frontend/src/app/[locale]/layout.tsx @@ -1,19 +1,11 @@ -import clsx from 'clsx'; import { hasLocale, NextIntlClientProvider } from 'next-intl'; import { getTranslations } from 'next-intl/server'; -import { DM_Mono } from 'next/font/google'; import { notFound } from 'next/navigation'; import { routing } from '@/i18n/routing'; import Footer from './_partials/footer'; import Header from './_partials/header'; +import { NewsletterBlock } from '@/components'; import './globals.css'; -import { CampaignBanner, NewsletterBlock } from '@/components'; - -const dmMono = DM_Mono({ - subsets: ['latin'], - weight: ['400'], - variable: '--font-secondary', -}); export async function generateMetadata({ params, @@ -68,32 +60,14 @@ export default async function RootLayout({ } return ( - - -