Repository: incubateur-ademe/nosgestesclimat-site-nextjs
Pull request #908: ✨ Rework partners page, block
Author: login: bjlaa
Created: 2025-04-02T10:34:59Z | Merged: 2025-04-15T08:18:41Z



Diff:
diff --git a/src/app/[locale]/(narrow-layout)/(pages-statiques)/nos-relais/page.tsx b/src/app/[locale]/(narrow-layout)/(pages-statiques)/nos-relais/page.tsx
index d30dd4e798..e9ca5d1a63 100644
--- a/src/app/[locale]/(narrow-layout)/(pages-statiques)/nos-relais/page.tsx
+++ b/src/app/[locale]/(narrow-layout)/(pages-statiques)/nos-relais/page.tsx
@@ -1,13 +1,18 @@
+import CategoryFilters from '@/components/filtering/CategoryFilters'
 import type { PartnerType } from '@/adapters/cmsClient'
 import Trans from '@/components/translation/trans/TransServer'
+import { FILTER_SEARCH_PARAM_KEY } from '@/constants/filtering'
 import InlineLink from '@/design-system/inputs/InlineLink'
 import Card from '@/design-system/layout/Card'
 import Title from '@/design-system/layout/Title'
+import Emoji from '@/design-system/utils/Emoji'
 import { getServerTranslation } from '@/helpers/getServerTranslation'
 import { t } from '@/helpers/metadata/fakeMetadataT'
 import { getCommonMetadata } from '@/helpers/metadata/getCommonMetadata'
 import { fetchPartners } from '@/services/cms/fetchPartners'
 import type { DefaultPageProps } from '@/types'
+import { encodeDottedNameAsURI } from '@/utils/format/encodeDottedNameAsURI'
+import type { DottedName } from '@incubateur-ademe/nosgestesclimat'
 import Image from 'next/image'
 
 export const generateMetadata = getCommonMetadata({
@@ -36,8 +41,15 @@ const getPartnersByCategory = (partners: PartnerType[]) => {
   )
 }
 
-export default async function NosRelais({ params }: DefaultPageProps) {
+export default async function NosRelais({
+  params,
+  searchParams,
+}: DefaultPageProps & {
+  searchParams: Promise<{ [FILTER_SEARCH_PARAM_KEY]: string }>
+}) {
   const { locale } = await params
+  const { [FILTER_SEARCH_PARAM_KEY]: categoryFilter } = await searchParams
+
   const { t } = await getServerTranslation({ locale })
 
   const partners = await fetchPartners()
@@ -48,41 +60,39 @@ export default async function NosRelais({ params }: DefaultPageProps) {
     <div>
       <Title>
         <Trans locale={locale}>Ils relaient </Trans>
+
         <span className="text-primary-700">
           <Trans locale={locale}>Nos Gestes Climat</Trans>
         </span>
       </Title>
 
-      <div className="flex flex-wrap items-center md:flex-nowrap md:gap-16">
-        <div>
+      <div className="mb-8 flex flex-col items-center md:flex-row md:flex-nowrap md:gap-16">
+        <div className="flex-1">
           <p>
+            <strong className="text-primary-700">
+              <Trans locale={locale}>Plusieurs milliers d’organisations</Trans>
+            </strong>{' '}
             <Trans locale={locale}>
-              Plus de 40 acteurs relaient ou ont relayé Nos Gestes Climat à
-              travers 
-              <a
-                href="https://accelerateur-transition-ecologique-ademe.notion.site/Int-grer-Nos-Gestes-Climat-en-iframe-abdeb175baf84143922006964d80348c"
-                target="_blank"
-                rel="noopener noreferrer">
-                l'intégration du calculateur
-              </a>{' '}
-              sur leur site internet ou sa diffusion via{' '}
-              <InlineLink href="/organisations">des campagnes</InlineLink>{' '}
-              (mail, réseaux sociaux et / ou affichage). C'est majoritairement
-              grâce à eux que nous sensibilisons près de 2 000 nouvelles
-              personnes en moyenne chaque jour et nous les en remercions.
+              partagent Nos Gestes Climat via leur site ou des campagnes (mails,
+              réseaux sociaux, affichage), nous permettant de sensibiliser près
+              de 2 000 personnes chaque jour. Un grand merci à eux !
             </Trans>
           </p>
 
           <p>
             <Trans locale={locale}>
-              Vous avez relayé Nos Gestes Climat et souhaitez apparaître dans
-              notre galerie de relais ? Merci de nous envoyer un message avec
-              votre logo via{' '}
-              <InlineLink href="/contact">notre page de contact</InlineLink>.
-            </Trans>
+              Vous relayez Nos Gestes Climat et souhaitez apparaître dans notre
+              galerie ?
+            </Trans>{' '}
+            <InlineLink className="inline" href="/contact">
+              <Trans locale={locale}>
+                Envoyez-nous votre logo via notre page de contact
+              </Trans>
+            </InlineLink>
           </p>
 
           <p className="mb-8 italic">
+            <Emoji>ℹ️</Emoji>{' '}
             <Trans locale={locale}>
               N.B. : aucun acteur cité ci-dessous ne finance Nos Gestes Climat,
               qui est et restera un service public, indépendant et gratuit de
@@ -90,6 +100,7 @@ export default async function NosRelais({ params }: DefaultPageProps) {
             </Trans>
           </p>
         </div>
+
         <Image
           width="200"
           height="400"
@@ -101,12 +112,25 @@ export default async function NosRelais({ params }: DefaultPageProps) {
         />
       </div>
 
-      {Object.entries(partnersByCategories).map(
-        ([category, partners]: [string, PartnerType[]]) => (
+      <CategoryFilters
+        categories={Object.keys(partnersByCategories).map((category: string) => ({
+          title: category,
+          dottedName: category as DottedName,
+          count: partnersByCategories[category].length,
+        }))}
+        className="mb-6"
+      />
+      {Object.keys(partnersByCategories)
+        .filter((category: string) =>
+          typeof categoryFilter !== 'undefined'
+            ? categoryFilter === encodeDottedNameAsURI(category)
+            : true
+        )
+        .map((category: string) => (
           <div key={category} className="mb-16">
             <h2>{category}</h2>
             <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
-              {partners.map((partner) => (
+              {partnersByCategories[category].map((partner) => (
                 <Card
                   key={partner.name}
                   href={partner.link}
@@ -122,19 +146,13 @@ export default async function NosRelais({ params }: DefaultPageProps) {
                   />
                   <p className="mb-4 font-bold">{partner.name}</p>
                   <p className="my-0 underline">
-                    {
-                      partner.link
-                        .replace('https://', '')
-                        .replace('www.', '')
-                        .split('/')[0]
-                    }
+                    {partner.link}
                   </p>
                 </Card>
               ))}
             </div>
           </div>
-        )
-      )}
+        ))}
     </div>
   )
 }
diff --git a/src/app/[locale]/(simulation)/(large-layout)/actions/_components/ActionsContent.tsx b/src/app/[locale]/(simulation)/(large-layout)/actions/_components/ActionsContent.tsx
index e79e7e8686..f0fca039c2 100644
--- a/src/app/[locale]/(simulation)/(large-layout)/actions/_components/ActionsContent.tsx
+++ b/src/app/[locale]/(simulation)/(large-layout)/actions/_components/ActionsContent.tsx
@@ -1,16 +1,19 @@
 'use client'
 
+import CategoryFilters from '@/components/filtering/CategoryFilters'
+import { FILTER_SEARCH_PARAM_KEY } from '@/constants/filtering'
 import getActions from '@/helpers/actions/getActions'
 import {
   useCurrentSimulation,
   useEngine,
   useTempEngine,
 } from '@/publicodes-state'
+import type { Action } from '@/publicodes-state/types'
+import { capitalizeString } from '@/utils/capitalizeString'
 import { useSearchParams } from 'next/navigation'
 import { useEffect, useRef, useState } from 'react'
 import Actions from './actionsContent/Actions'
 import AllerPlusLoin from './actionsContent/AllerPlusLoin'
-import CategoryFilters from './actionsContent/CategoryFilters'
 import OptionBar from './actionsContent/OptionBar'
 
 export default function ActionsContent() {
@@ -21,12 +24,14 @@ export default function ActionsContent() {
 
   const searchParams = useSearchParams()
 
-  const category = searchParams.get('catégorie')
+  const category = searchParams.get(FILTER_SEARCH_PARAM_KEY)
 
   const { actionChoices, progression } = useCurrentSimulation()
 
   const { rules, getSpecialRuleObject } = useTempEngine()
 
+  const { categories } = useEngine()
+
   const actions = getActions({
     rules,
     radical,
@@ -84,7 +89,15 @@ export default function ActionsContent() {
       } text-center`}
       aria-hidden={isSimulationWellStarted ? false : true}>
       <div className="relative">
-        <CategoryFilters actions={actionsDisplayed} />
+        <CategoryFilters
+          categories={categories.map((category) => ({
+            title: capitalizeString(category) ?? '',
+            dottedName: category,
+            count: actionsDisplayed.filter((action: Action) =>
+              action.dottedName.startsWith(category)
+            ).length,
+          }))}
+        />
 
         <OptionBar
           setRadical={setRadical}
diff --git a/src/app/[locale]/(simulation)/(large-layout)/actions/_components/actionsContent/CategoryFilters.tsx b/src/app/[locale]/(simulation)/(large-layout)/actions/_components/actionsContent/CategoryFilters.tsx
deleted file mode 100644
index 651e85b57d..0000000000
--- a/src/app/[locale]/(simulation)/(large-layout)/actions/_components/actionsContent/CategoryFilters.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-'use client'
-
-import { useEngine } from '@/publicodes-state'
-import Filter from './categoryFilters/Filter'
-
-type Props = {
-  actions: any
-}
-
-export default function CategoryFilters({ actions }: Props) {
-  const { getCategory, categories } = useEngine()
-
-  const countByCategory = actions.reduce((accumulator: any, action: any) => {
-    const category = getCategory(action.dottedName)
-
-    return !category
-      ? accumulator
-      : { ...accumulator, [category]: (accumulator[category] || 0) + 1 }
-  }, {})
-
-  return (
-    <ul className="flex list-none flex-wrap justify-center gap-1 pl-0">
-      {categories?.map((category) => {
-        return (
-          <Filter
-            key={category}
-            dottedName={category}
-            countByCategory={countByCategory}
-          />
-        )
-      })}
-    </ul>
-  )
-}
diff --git a/src/app/[locale]/(simulation)/(large-layout)/actions/_components/actionsContent/categoryFilters/Filter.tsx b/src/app/[locale]/(simulation)/(large-layout)/actions/_components/actionsContent/categoryFilters/Filter.tsx
deleted file mode 100644
index 2895902d2d..0000000000
--- a/src/app/[locale]/(simulation)/(large-layout)/actions/_components/actionsContent/categoryFilters/Filter.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-'use client'
-
-import { actionsClickFilter } from '@/constants/tracking/pages/actions'
-import {
-  getBackgroundColor,
-  getBackgroundLightColor,
-  getTextDarkColor,
-} from '@/helpers/getCategoryColorClass'
-import { useRule } from '@/publicodes-state'
-import { trackEvent } from '@/utils/analytics/trackEvent'
-import type { DottedName } from '@incubateur-ademe/nosgestesclimat'
-import { useRouter, useSearchParams } from 'next/navigation'
-
-type Props = {
-  dottedName: DottedName
-  countByCategory: any
-}
-
-export default function Filter({ dottedName, countByCategory }: Props) {
-  const { title } = useRule(dottedName)
-
-  const router = useRouter()
-
-  const metric = useSearchParams().get('métrique') || ''
-  const categorySelected = useSearchParams().get('catégorie') || ''
-
-  const isSelected = categorySelected === dottedName
-
-  const buildURL = () => {
-    const siteURL = `${window.location.origin}${window.location.pathname}`
-
-    const searchParamsStart = metric || !isSelected ? '?' : ''
-
-    const metricSearchParam = metric ? `métrique=${metric}&` : ''
-
-    const searchParamsPart = `${searchParamsStart}${metricSearchParam}${
-      isSelected ? '' : `catégorie=${dottedName}`
-    }`
-
-    return `${siteURL}${searchParamsPart}`
-  }
-
-  return (
-    <li
-      className={`height-[1.8rem] rounded-md ${
-        !categorySelected || categorySelected === dottedName
-          ? getBackgroundLightColor(dottedName)
-          : 'bg-gray-200'
-      }`}
-      style={{
-        backgroundColor: getBackgroundColor(),
-      }}>
-      <button
-        className={`p-2 text-xs font-bold ${getTextDarkColor(dottedName)}`}
-        onClick={() => {
-          trackEvent(actionsClickFilter(dottedName))
-          router.replace(buildURL(), {
-            scroll: false,
-          })
-        }}>
-        {title}{' '}
-        <span
-          className={`ml-2 inline-block w-4 rounded-full bg-white ${getTextDarkColor(dottedName)}`}>
-          {countByCategory[dottedName] || 0}
-        </span>
-      </button>
-    </li>
-  )
-}
diff --git a/src/app/[locale]/(simulation)/(large-layout)/organisations/page.tsx b/src/app/[locale]/(simulation)/(large-layout)/organisations/page.tsx
index 64cd9581a7..743024e2c3 100644
--- a/src/app/[locale]/(simulation)/(large-layout)/organisations/page.tsx
+++ b/src/app/[locale]/(simulation)/(large-layout)/organisations/page.tsx
@@ -1,9 +1,8 @@
+import TheySpeakAboutUs from '@/app/[locale]/_components/TheySpeakAboutUs'
 import Separator from '@/design-system/layout/Separator'
 import type { DefaultPageProps } from '@/types'
-import CTAFooter from './_components/CTAFooter'
 import HeroSection from './_components/HeroSection'
 import IllustratedPointsList from './_components/IllustratedPointsList'
-import PartnersSection from './_components/PartnersSection'
 
 export default async function Page({ params }: DefaultPageProps) {
   const { locale } = await params
@@ -17,8 +16,12 @@ export default async function Page({ params }: DefaultPageProps) {
 
         <IllustratedPointsList locale={locale} />
       </div>
-      <PartnersSection locale={locale} />
-      <CTAFooter />
+
+      <TheySpeakAboutUs
+        ctaHref="/organisations/creer"
+        className="mt-16"
+        locale={locale}
+      />
     </section>
   )
 }
diff --git a/src/app/[locale]/_components/TheySpeakAboutUs.tsx b/src/app/[locale]/_components/TheySpeakAboutUs.tsx
index 6e6e59bfce..7dce9afbec 100644
--- a/src/app/[locale]/_components/TheySpeakAboutUs.tsx
+++ b/src/app/[locale]/_components/TheySpeakAboutUs.tsx
@@ -1,13 +1,27 @@
 import Background from '@/components/landing-pages/Background'
+import ButtonLink from '@/design-system/inputs/ButtonLink'
 import Trans from '@/components/translation/trans/TransServer'
 import { fetchPartners } from '@/services/cms/fetchPartners'
 import Image from 'next/image'
+import { twMerge } from 'tailwind-merge'
 
-export default async function TheySpeakAboutUs({ locale }: { locale: string }) {
+export default async function TheySpeakAboutUs({
+  locale,
+  className,
+  ctaHref = '/nos-relais',
+}: {
+  locale: string
+  className?: string
+  ctaHref?: string
+}) {
   const partners = await fetchPartners({ displayOnLandingPage: true })
 
   return (
-    <div className="relative mb-10 px-4 py-16 md:mb-20 md:py-28 xl:mb-32">
+    <div
+      className={twMerge(
+        'relative mb-10 px-4 py-16 md:mb-20 md:py-28 xl:mb-32',
+        className
+      )}>
       {/* Helps cover the triangles of white shown because of the perspective change in Background */}
       <div className="bg-heroLightBackground absolute top-0 left-0 h-1/2 w-[200%]" />
 
@@ -19,8 +33,11 @@ export default async function TheySpeakAboutUs({ locale }: { locale: string }) {
       />
 
       <div className="relative flex flex-col items-center gap-10 md:mx-auto md:max-w-5xl">
-        <h2 className="text-center text-2xl md:text-3xl">
-          <Trans locale={locale}>Ils parlent de nous</Trans>
+        <h2 className="text-center text-xl md:text-2xl">
+          <Trans locale={locale}>
+            Plusieurs milliers d’organisations nous font confiance pour
+            sensibiliser efficacement
+          </Trans>
         </h2>
 
         <ul className="flex w-full flex-row flex-wrap items-center justify-center gap-7 md:justify-between">
@@ -36,6 +53,12 @@ export default async function TheySpeakAboutUs({ locale }: { locale: string }) {
             </li>
           ))}
         </ul>
+
+        <div className="flex justify-center">
+          <ButtonLink color="secondary" size="xl" href={ctaHref}>
+            <Trans locale={locale}>Rejoignez-les</Trans>
+          </ButtonLink>
+        </div>
       </div>
     </div>
   )
diff --git a/src/components/filtering/CategoryFilters.tsx b/src/components/filtering/CategoryFilters.tsx
new file mode 100644
index 0000000000..2a3df3c2be
--- /dev/null
+++ b/src/components/filtering/CategoryFilters.tsx
@@ -0,0 +1,35 @@
+'use client'
+
+import type { DottedName } from '@incubateur-ademe/nosgestesclimat'
+import { twMerge } from 'tailwind-merge'
+import CategoryFilter from './categoryFilters/CategoryFilter'
+
+type Props = {
+  categories: {
+    title: string
+    dottedName: DottedName
+    count: number
+  }[]
+  className?: string
+}
+
+export default function CategoryFilters({ categories, className }: Props) {
+  return (
+    <ul
+      className={twMerge(
+        'flex list-none flex-wrap justify-start gap-1 pl-0',
+        className
+      )}>
+      {categories?.map(({ title, dottedName, count }) => {
+        return (
+          <CategoryFilter
+            key={title}
+            title={title}
+            dottedName={dottedName}
+            count={count}
+          />
+        )
+      })}
+    </ul>
+  )
+}
diff --git a/src/components/filtering/categoryFilters/CategoryFilter.tsx b/src/components/filtering/categoryFilters/CategoryFilter.tsx
new file mode 100644
index 0000000000..c5e7ae84de
--- /dev/null
+++ b/src/components/filtering/categoryFilters/CategoryFilter.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import { FILTER_SEARCH_PARAM_KEY } from '@/constants/filtering'
+import { trackingCategoryFilter } from '@/constants/tracking/misc'
+import {
+  getBackgroundColor,
+  getBackgroundLightColor,
+  getTextDarkColor,
+} from '@/helpers/getCategoryColorClass'
+import { trackEvent } from '@/utils/analytics/trackEvent'
+import { encodeDottedNameAsURI } from '@/utils/format/encodeDottedNameAsURI'
+import type { DottedName } from '@incubateur-ademe/nosgestesclimat'
+import { useRouter, useSearchParams } from 'next/navigation'
+
+type Props = {
+  title: string
+  dottedName: DottedName
+  count: number
+}
+
+export default function CategoryFilter({ title, dottedName, count }: Props) {
+  const router = useRouter()
+
+  const categorySelected = useSearchParams().get(FILTER_SEARCH_PARAM_KEY) || ''
+
+  const encodedDottedName = encodeDottedNameAsURI(dottedName)
+
+  const isSelected = categorySelected === encodedDottedName
+
+  const buildURL = () => {
+    const siteURL = new URL(
+      `${window.location.origin}${window.location.pathname}`
+    )
+
+    if (isSelected && encodedDottedName === categorySelected) {
+      siteURL.searchParams.delete(FILTER_SEARCH_PARAM_KEY)
+    }
+
+    if (isSelected && encodedDottedName !== categorySelected) {
+      siteURL.searchParams.set(FILTER_SEARCH_PARAM_KEY, encodedDottedName)
+    }
+
+    if (!isSelected) {
+      siteURL.searchParams.append(FILTER_SEARCH_PARAM_KEY, encodedDottedName)
+    }
+
+    return siteURL.toString()
+  }
+
+  return (
+    <li
+      className={`height-[1.8rem] rounded-md ${
+        !categorySelected || isSelected
+          ? getBackgroundLightColor(dottedName)
+          : 'bg-gray-200'
+      }`}
+      style={{
+        backgroundColor: getBackgroundColor(),
+      }}>
+      <button
+        className={`p-2 text-xs font-bold ${getTextDarkColor(dottedName)}`}
+        onClick={() => {
+          trackEvent(
+            trackingCategoryFilter(dottedName, window.location.pathname)
+          )
+          router.replace(buildURL(), {
+            scroll: false,
+          })
+        }}>
+        {title}{' '}
+        <span
+          className={`ml-2 inline-block w-4 rounded-full bg-white ${getTextDarkColor(dottedName)}`}>
+          {count ?? 0}
+        </span>
+      </button>
+    </li>
+  )
+}
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
index 0cb11e556b..92762f0707 100644
--- a/src/components/layout/Footer.tsx
+++ b/src/components/layout/Footer.tsx
@@ -29,6 +29,13 @@ import Logo from '../misc/Logo'
 import LanguageSwitchButton from '../translation/LanguageSwitchButton'
 import Trans from '../translation/trans/TransClient'
 
+const WHITE_BACKGROUND_PATHS = [
+  '/empreinte-eau',
+  '/empreinte-carbone',
+  '/blog',
+  '/organisations',
+]
+
 export default function Footer({ className = '' }) {
   const pathname = usePathname()
   const locale = useLocale()
@@ -40,9 +47,7 @@ export default function Footer({ className = '' }) {
   const shouldUseWhiteBackground =
     pathname === '/' ||
     pathname === `/${locale}` ||
-    pathname.includes('/empreinte-eau') ||
-    pathname.includes('/empreinte-carbone') ||
-    pathname.includes('/blog')
+    WHITE_BACKGROUND_PATHS.includes(pathname)
 
   return (
     <footer
diff --git a/src/constants/filtering.ts b/src/constants/filtering.ts
new file mode 100644
index 0000000000..7994dad700
--- /dev/null
+++ b/src/constants/filtering.ts
@@ -0,0 +1 @@
+export const FILTER_SEARCH_PARAM_KEY = 'category'
diff --git a/src/constants/tracking/misc.ts b/src/constants/tracking/misc.ts
index 1af6209359..69ae63fac3 100644
--- a/src/constants/tracking/misc.ts
+++ b/src/constants/tracking/misc.ts
@@ -1,6 +1,8 @@
 // Return tracking data in format
 // [ 'trackEvent', 'Category', 'Action', 'Name', 'Value' ]
 
+import type { DottedName } from '@incubateur-ademe/nosgestesclimat'
+
 export const trackingIframeVisit = (url: string) => [
   'trackEvent',
   'Misc',
@@ -49,3 +51,11 @@ export const trackingSplitTesting = (branch: string) => [
 
 // Banner
 export const trackingBannerClick = ['trackEvent', 'Bannière', 'Click lien']
+
+// Category filter
+export const trackingCategoryFilter = (category: DottedName, path: string) => [
+  'trackEvent',
+  path,
+  'Category filter',
+  `Click Filter ${category}`,
+]
diff --git a/src/constants/tracking/pages/actions.ts b/src/constants/tracking/pages/actions.ts
index cce470abc6..8586f31739 100644
--- a/src/constants/tracking/pages/actions.ts
+++ b/src/constants/tracking/pages/actions.ts
@@ -6,14 +6,6 @@ import type { DottedName } from '@incubateur-ademe/nosgestesclimat'
 // Figma comment #67
 export const actionsClickStart = ['trackEvent', 'Actions', 'Click Démarrer']
 
-// Figma comment #68
-export const actionsClickFilter = (category: DottedName) => [
-  'trackEvent',
-  'Actions',
-  'Click Filter',
-  `Click Filter ${category}`,
-]
-
 // Figma comment #69
 export const actionsClickYes = (action: DottedName) => [
   'trackEvent',
diff --git a/src/helpers/actions/getActions.ts b/src/helpers/actions/getActions.ts
index 8d93161b1e..903155c0f6 100644
--- a/src/helpers/actions/getActions.ts
+++ b/src/helpers/actions/getActions.ts
@@ -1,4 +1,5 @@
 import getSomme from '@/publicodes-state/helpers/getSomme'
+import type { Action } from '@/publicodes-state/types'
 import { getCorrectedValue } from '@/utils/getCorrectedValue'
 import { sortBy } from '@/utils/sortBy'
 import type {
@@ -25,7 +26,7 @@ export default function getActions({
   safeEvaluate,
   getSpecialRuleObject,
   actionChoices,
-}: Props) {
+}: Props): Action[] {
   if (!rules) return []
 
   const actionsObject = rules.actions
@@ -50,9 +51,9 @@ export default function getActions({
   }) as NGCRule[]
 
   const sortedActionsByImpact = sortBy(
-    (value: unknown) =>
+    (value) =>
       (radical ? 1 : -1) * (getCorrectedValue(value as EvaluatedNode) || 1)
-  )(relevantActions)
+  )(relevantActions) as Action[]
 
   // Filter disabled actions
   return sortedActionsByImpact.filter((action: any) => {
diff --git a/src/locales/ambassadeurs/fr/ambassadeurs.yaml b/src/locales/ambassadeurs/fr/ambassadeurs.yaml
index 06098e8136..3babf3731c 100644
--- a/src/locales/ambassadeurs/fr/ambassadeurs.yaml
+++ b/src/locales/ambassadeurs/fr/ambassadeurs.yaml
@@ -108,15 +108,18 @@ Entreprises:
     image: steply.png
     link: https://steply-app.com/
 Collectivités et acteurs publics:
-  - title: 'Ville de Grenoble'
+  - title: Ville de Grenoble
     image: Grenoble.png
     link: https://www.grenoble.fr/593-grenoble-climat.htm
-  - title: 'Grand Paris Seine-Ouest'
+  - title: Grand Paris Seine-Ouest
     image: paris-ouest.png
     link: https://www.seineouest.fr/nos-gestes-climat
   - title: Ville de Lyon
     image: lyon.png
     link: https://www.lyon.fr/actualite/environnement/carbone-detox-le-defi-pour-reduire-son-impact
+  - title: Strasbourg Métropole
+    image: https://nosgestesclimat-prod.s3.fr-par.scw.cloud/cms/strasbourg_d05212223e.jpg
+    link: https://www.strasbourg.eu/
   - title: 'La Rochelle'
     image: la rochelle.png
     link: https://www.larochelle-zerocarbone.fr/s-engager/la-boite-a-outils-du-zero-carbone/nos-gestes-climat
@@ -147,7 +150,7 @@ Associations:
     link: https://www.weareclimates.org/
 Education:
   - title: 'Université Catholique de Lyon'
-    image: ucl.png
+    image: https://nosgestesclimat-prod.s3.fr-par.scw.cloud/cms/ucly_d769ca6466.png
     link: https://www.ucly.fr/
   - title: 'L’académie du climat'
     image: academieduclimat.png
@@ -156,9 +159,9 @@ Education:
     image: CNED.png
     link: https://www.cned.fr/transition-ecologique/ba-ba-du-climat-et-de-la-biodiversite
 Lieux et événements:
-  - title: 'Silicet'
-    image: silicetbidule.png
-    link: https://scilicet.fr/
   - title: 'SoGood Festival'
     image: sogood.png
     link: https://www.sogoodfestival.com/
+  - title: 'Silicet'
+    image: https://nosgestesclimat-prod.s3.fr-par.scw.cloud/cms/silicet_6d2b068bee.jpg
+    link: https://scilicet.fr/
diff --git a/src/locales/ui/ui-en.yaml b/src/locales/ui/ui-en.yaml
index 769c91deae..c9f7b94c27 100644
--- a/src/locales/ui/ui-en.yaml
+++ b/src/locales/ui/ui-en.yaml
@@ -1652,6 +1652,11 @@ entries:
   Ouvrir: Open
   Nombre de jours, semaines ou mois à afficher: Number of days, weeks or months to display
   simulations terminées ce mois-ci (en cours): simulations completed this month (in progress)
+  Plusieurs milliers d’organisations: Several thousand organisations
+  Vous relayez Nos Gestes Climat et souhaitez apparaître dans notre galerie ?: Do you relay Nos Gestes Climat and want to appear in our gallery?
+  Aucun de ces acteurs ne finance Nos Gestes Climat, un service public, gratuit et indépendant de l'ADEME.: None of these players finances Nos Gestes Climat, a free and independent public service provided by ADEME.
+  Envoyez-nous votre logo via notre page de contact: Send us your logo via our contact page
+  partagent Nos Gestes Climat via leur site ou des campagnes (mails, réseaux sociaux, affichage), nous permettant de sensibiliser près de 2 000 personnes chaque jour. Un grand merci à eux !: share Nos Gestes Climat via their website or campaigns (email, social networks, posters), enabling us to raise awareness among almost 2,000 people every day. Many thanks to them!
   Simulation mise à jour: Updated simulation
   Calculateur d’empreinte carbone et eau - Nos Gestes Climat: Carbon and water footprint calculator - Nos Gestes Climat
   Demander une démo de notre calculateur pour les organisations - Nos Gestes Climat: Request a demo of our calculator for organisations - Nos Gestes Climat
diff --git a/src/locales/ui/ui-es.yaml b/src/locales/ui/ui-es.yaml
index 3e96b0e35d..bbc077cda3 100644
--- a/src/locales/ui/ui-es.yaml
+++ b/src/locales/ui/ui-es.yaml
@@ -1645,6 +1645,11 @@ entries:
   Sélectionner l'empreinte carbone, voir le détail ci-dessous: Seleccione la huella de carbono, ver detalles más abajo
   Nombre de jours, semaines ou mois à afficher: Número de días, semanas o meses a visualizar
   simulations terminées ce mois-ci (en cours): simulaciones realizadas este mes (en curso)
+  partagent Nos Gestes Climat via leur site ou des campagnes (mails, réseaux sociaux, affichage), nous permettant de sensibiliser près de 2 000 personnes chaque jour. Un grand merci à eux !: comparten Nos Gestes Climat a través de su página web o de campañas (correo electrónico, redes sociales, carteles), lo que nos permite sensibilizar a casi 2.000 personas cada día. ¡Muchas gracias a todos ellos!
+  Envoyez-nous votre logo via notre page de contact: Envíenos su logotipo a través de nuestra página de contacto
+  Plusieurs milliers d’organisations: Varios miles de organizaciones
+  Aucun de ces acteurs ne finance Nos Gestes Climat, un service public, gratuit et indépendant de l'ADEME.: Ninguno de estos actores financia Nos Gestes Climat, un servicio público, gratuito e independiente prestado por la ADEME.
+  Vous relayez Nos Gestes Climat et souhaitez apparaître dans notre galerie ?: ¿Relevas Nos Gestes Climat y quieres aparecer en nuestra galería?
   Simulation mise à jour: Simulación actualizada
   Mon groupe - Nos Gestes Climat: Mi Grupo - Nos Gestes Climat
   International - Nos Gestes Climat: Internacional - Nos Gestes Climat
diff --git a/src/locales/ui/ui-fr.yaml b/src/locales/ui/ui-fr.yaml
index dc5f94ebc0..b2fa5b3f86 100644
--- a/src/locales/ui/ui-fr.yaml
+++ b/src/locales/ui/ui-fr.yaml
@@ -1217,6 +1217,11 @@ entries:
   Voir le détail ci-dessous: Voir le détail ci-dessous
   Découvrir: Découvrir
   simulations terminées ce mois-ci (en cours): simulations terminées ce mois-ci (en cours)
+  Aucun de ces acteurs ne finance Nos Gestes Climat, un service public, gratuit et indépendant de l'ADEME.: Aucun de ces acteurs ne finance Nos Gestes Climat, un service public, gratuit et indépendant de l'ADEME.
+  Envoyez-nous votre logo via notre page de contact: Envoyez-nous votre logo via notre page de contact
+  partagent Nos Gestes Climat via leur site ou des campagnes (mails, réseaux sociaux, affichage), nous permettant de sensibiliser près de 2 000 personnes chaque jour. Un grand merci à eux !: partagent Nos Gestes Climat via leur site ou des campagnes (mails, réseaux sociaux, affichage), nous permettant de sensibiliser près de 2 000 personnes chaque jour. Un grand merci à eux !
+  Plusieurs milliers d’organisations: Plusieurs milliers d’organisations
+  Vous relayez Nos Gestes Climat et souhaitez apparaître dans notre galerie ?: Vous relayez Nos Gestes Climat et souhaitez apparaître dans notre galerie ?
   Simulation mise à jour: Simulation mise à jour
   Calculateur d’empreinte carbone et eau - Nos Gestes Climat: Calculateur d’empreinte carbone et eau - Nos Gestes Climat
   Créer un groupe, étape 1 sur 2 - Nos Gestes Climat: Créer un groupe, étape 1 sur 2 - Nos Gestes Climat
diff --git a/src/publicodes-state/types.d.ts b/src/publicodes-state/types.d.ts
index 9bbd7ec331..5746ea45a1 100644
--- a/src/publicodes-state/types.d.ts
+++ b/src/publicodes-state/types.d.ts
@@ -109,3 +109,8 @@ export type FormattedSuggestion = {
   label: string
   value: SuggestionValue | Record<string, SuggestionValue>
 }
+
+export type Action = {
+  dottedName: DottedName
+  value: number
+}
diff --git a/src/utils/format/encodeDottedNameAsURI.ts b/src/utils/format/encodeDottedNameAsURI.ts
new file mode 100644
index 0000000000..4e79ea982d
--- /dev/null
+++ b/src/utils/format/encodeDottedNameAsURI.ts
@@ -0,0 +1,3 @@
+export const encodeDottedNameAsURI = (dottedName: string) => {
+  return encodeURIComponent(dottedName.toLowerCase())
+}


Reviews:
user: login: paulsouche
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T09:51:37Z
user: login: paulsouche
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T12:23:37Z
user: login: bjlaa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T12:24:18Z
user: login: paulsouche
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T12:32:58Z

Review comments:
path: public/images/ambassadeurs/silicet.jpg
user: login: paulsouche
body: A basculer sur le CMS
created_at: 2025-04-07T09:50:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,88 +47,99 @@ export default async function NosRelais({ params }: DefaultPageProps) {
         </span>
       </Title>
 
-      <div className="flex flex-wrap items-center md:flex-nowrap md:gap-16">
-        <div>
+      <div className="mb-8 flex flex-col items-center md:flex-row md:flex-nowrap md:gap-16">
+        <div className="flex-1">
           <p>
+            <strong className="text-primary-700">
+              <Trans locale={locale}>Plusieurs milliers d’organisations</Trans>
+            </strong>{' '}
             <Trans locale={locale}>
-              Plus de 40 acteurs relaient ou ont relayé Nos Gestes Climat à
-              travers 
-              <a
-                href="https://accelerateur-transition-ecologique-ademe.notion.site/Int-grer-Nos-Gestes-Climat-en-iframe-abdeb175baf84143922006964d80348c"
-                target="_blank"
-                rel="noopener noreferrer">
-                l’intégration du calculateur
-              </a>{' '}
-              sur leur site internet ou sa diffusion via{' '}
-              <InlineLink href="/organisations">des campagnes</InlineLink>{' '}
-              (mail, réseaux sociaux et / ou affichage). C’est majoritairement
-              grâce à eux que nous sensibilisons près de 2 000 nouvelles
-              personnes en moyenne chaque jour et nous les en remercions.
+              partagent Nos Gestes Climat via leur site ou des campagnes (mails,
+              réseaux sociaux, affichage), nous permettant de sensibiliser près
+              de 2 000 personnes chaque jour. Un grand merci à eux !
             </Trans>
           </p>
 
           <p>
             <Trans locale={locale}>
-              Vous avez relayé Nos Gestes Climat et souhaitez apparaître dans
-              notre galerie de relais ? Merci de nous envoyer un message avec
-              votre logo via{' '}
-              <InlineLink href="/contact">notre page de contact</InlineLink>.
-            </Trans>
+              Vous relayez Nos Gestes Climat et souhaitez apparaître dans notre
+              galerie ?
+            </Trans>{' '}
+            <InlineLink className="inline" href="/contact">
+              <Trans locale={locale}>
+                Envoyez-nous votre logo via notre page de contact
+              </Trans>
+            </InlineLink>
           </p>
 
           <p className="mb-8 italic">
+            <Emoji>ℹ️</Emoji>{' '}
             <Trans locale={locale}>
-              N.B. : aucun acteur cité ci-dessous ne finance Nos Gestes Climat,
-              qui est et restera un service public, indépendant et gratuit de
-              l’ADEME.
+              Aucun de ces acteurs ne finance Nos Gestes Climat, un service
+              public, gratuit et indépendant de l'ADEME.
             </Trans>
           </p>
         </div>
         <Image
-          width="300"
-          height="400"
-          className="ml-auto w-48 self-start md:-mt-16 md:w-auto"
+          width="240"
+          height="300"
+          className="ml-auto max-w-56 self-start md:-mt-16 md:w-auto md:max-w-80"
           alt={t(
             'Un grand-père et sa petite-fille au cinéma, mangeant du pop-corn.'
           )}
           src="/images/ambassadeurs/illu-cinema.svg"
         />
       </div>
 
-      {categories.map((category: any) => (
-        <div key={category} className="mb-16">
-          <h2>{category}</h2>
-          <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
-            {ambassadeurs[category].map((ambassadeur: any) => (
-              <Card
-                key={ambassadeur.title}
-                href={ambassadeur.link}
-                tag="a"
-                className="border-none bg-primary-50 no-underline"
-                target="_blank">
-                <Image
-                  src={'/images/ambassadeurs/' + ambassadeur.image}
-                  width="100"
-                  height="100"
-                  className="mx-auto mb-4 h-36 w-2/3 object-contain"
-                  alt={ambassadeur.title}
-                />
-                <p className="mb-4 font-bold">{ambassadeur.title}</p>
-                {ambassadeur.link ? (
-                  <p className="my-0 underline">
-                    {
-                      ambassadeur.link
-                        .replace('https://', '')
-                        .replace('www.', '')
-                        .split('/')[0]
-                    }
-                  </p>
-                ) : null}
-              </Card>
-            ))}
+      <CategoryFilters
+        categories={categories.map((category: string) => ({
+          title: category,
+          dottedName: category as DottedName,
+          count: ambassadeurs[category].length,
+        }))}
+        className="mb-6"
+      />
+
+      {categories
+        .filter((category: string) =>
+          typeof categoryFilter !== 'undefined'
+            ? categoryFilter === encodeDottedNameAsURI(category)
+            : true
+        )
+        .map((category: any) => (
+          <div key={category} className="mb-16">
+            <h2>{category}</h2>
+            <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
+              {ambassadeurs[category].map((ambassadeur: any) => (
+                <Card
+                  key={ambassadeur.title}
+                  href={ambassadeur.link}
+                  tag="a"
+                  className="bg-primary-50 border-none no-underline"
+                  target="_blank">
+                  <Image
+                    src={'/images/ambassadeurs/' + ambassadeur.image}
path: src/app/[locale]/(narrow-layout)/(pages-statiques)/nos-relais/page.tsx
user: login: paulsouche
body: Does not work for migrated partners
created_at: 2025-04-14T12:23:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,88 +47,99 @@ export default async function NosRelais({ params }: DefaultPageProps) {
         </span>
       </Title>
 
-      <div className="flex flex-wrap items-center md:flex-nowrap md:gap-16">
-        <div>
+      <div className="mb-8 flex flex-col items-center md:flex-row md:flex-nowrap md:gap-16">
+        <div className="flex-1">
           <p>
+            <strong className="text-primary-700">
+              <Trans locale={locale}>Plusieurs milliers d’organisations</Trans>
+            </strong>{' '}
             <Trans locale={locale}>
-              Plus de 40 acteurs relaient ou ont relayé Nos Gestes Climat à
-              travers 
-              <a
-                href="https://accelerateur-transition-ecologique-ademe.notion.site/Int-grer-Nos-Gestes-Climat-en-iframe-abdeb175baf84143922006964d80348c"
-                target="_blank"
-                rel="noopener noreferrer">
-                l’intégration du calculateur
-              </a>{' '}
-              sur leur site internet ou sa diffusion via{' '}
-              <InlineLink href="/organisations">des campagnes</InlineLink>{' '}
-              (mail, réseaux sociaux et / ou affichage). C’est majoritairement
-              grâce à eux que nous sensibilisons près de 2 000 nouvelles
-              personnes en moyenne chaque jour et nous les en remercions.
+              partagent Nos Gestes Climat via leur site ou des campagnes (mails,
+              réseaux sociaux, affichage), nous permettant de sensibiliser près
+              de 2 000 personnes chaque jour. Un grand merci à eux !
             </Trans>
           </p>
 
           <p>
             <Trans locale={locale}>
-              Vous avez relayé Nos Gestes Climat et souhaitez apparaître dans
-              notre galerie de relais ? Merci de nous envoyer un message avec
-              votre logo via{' '}
-              <InlineLink href="/contact">notre page de contact</InlineLink>.
-            </Trans>
+              Vous relayez Nos Gestes Climat et souhaitez apparaître dans notre
+              galerie ?
+            </Trans>{' '}
+            <InlineLink className="inline" href="/contact">
+              <Trans locale={locale}>
+                Envoyez-nous votre logo via notre page de contact
+              </Trans>
+            </InlineLink>
           </p>
 
           <p className="mb-8 italic">
+            <Emoji>ℹ️</Emoji>{' '}
             <Trans locale={locale}>
-              N.B. : aucun acteur cité ci-dessous ne finance Nos Gestes Climat,
-              qui est et restera un service public, indépendant et gratuit de
-              l’ADEME.
+              Aucun de ces acteurs ne finance Nos Gestes Climat, un service
+              public, gratuit et indépendant de l'ADEME.
             </Trans>
           </p>
         </div>
         <Image
-          width="300"
-          height="400"
-          className="ml-auto w-48 self-start md:-mt-16 md:w-auto"
+          width="240"
+          height="300"
+          className="ml-auto max-w-56 self-start md:-mt-16 md:w-auto md:max-w-80"
           alt={t(
             'Un grand-père et sa petite-fille au cinéma, mangeant du pop-corn.'
           )}
           src="/images/ambassadeurs/illu-cinema.svg"
         />
       </div>
 
-      {categories.map((category: any) => (
-        <div key={category} className="mb-16">
-          <h2>{category}</h2>
-          <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
-            {ambassadeurs[category].map((ambassadeur: any) => (
-              <Card
-                key={ambassadeur.title}
-                href={ambassadeur.link}
-                tag="a"
-                className="border-none bg-primary-50 no-underline"
-                target="_blank">
-                <Image
-                  src={'/images/ambassadeurs/' + ambassadeur.image}
-                  width="100"
-                  height="100"
-                  className="mx-auto mb-4 h-36 w-2/3 object-contain"
-                  alt={ambassadeur.title}
-                />
-                <p className="mb-4 font-bold">{ambassadeur.title}</p>
-                {ambassadeur.link ? (
-                  <p className="my-0 underline">
-                    {
-                      ambassadeur.link
-                        .replace('https://', '')
-                        .replace('www.', '')
-                        .split('/')[0]
-                    }
-                  </p>
-                ) : null}
-              </Card>
-            ))}
+      <CategoryFilters
+        categories={categories.map((category: string) => ({
+          title: category,
+          dottedName: category as DottedName,
+          count: ambassadeurs[category].length,
+        }))}
+        className="mb-6"
+      />
+
+      {categories
+        .filter((category: string) =>
+          typeof categoryFilter !== 'undefined'
+            ? categoryFilter === encodeDottedNameAsURI(category)
+            : true
+        )
+        .map((category: any) => (
+          <div key={category} className="mb-16">
+            <h2>{category}</h2>
+            <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
+              {ambassadeurs[category].map((ambassadeur: any) => (
+                <Card
+                  key={ambassadeur.title}
+                  href={ambassadeur.link}
+                  tag="a"
+                  className="bg-primary-50 border-none no-underline"
+                  target="_blank">
+                  <Image
+                    src={'/images/ambassadeurs/' + ambassadeur.image}
+                    width="100"
+                    height="100"
+                    className="mx-auto mb-4 h-36 w-2/3 object-contain"
+                    alt={ambassadeur.title}
+                  />
+                  <p className="mb-4 font-bold">{ambassadeur.title}</p>
+                  {ambassadeur.link ? (
+                    <p className="my-0 underline">
+                      {
+                        ambassadeur.link
+                          .replace('https://', '')
+                          .replace('www.', '')
+                          .split('/')[0]
path: src/app/[locale]/(narrow-layout)/(pages-statiques)/nos-relais/page.tsx
user: login: paulsouche
body: Keep that in mind when migrating to CMS
created_at: 2025-04-14T12:24:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,54 +50,60 @@ export default function TheySpeakAboutUs({ locale }: { locale: string }) {
 
           <li>
             <Image
-              src="/images/ambassadeurs/france_inter.png"
-              alt="France Inter"
-              className="h-auto w-16 md:w-24"
-              width={100}
-              height={100}
+              src="/images/ambassadeurs/leboncoin.png"
+              alt="Leboncoin"
+              className="h-auto w-24 md:w-36"
+              width={150}
+              height={150}
             />
           </li>
 
           <li>
             <Image
-              src="/images/ambassadeurs/bon-pote-no-background.svg"
-              alt="Bon Pote"
+              src="/images/ambassadeurs/france_info.png"
+              alt="France Info"
               className="h-auto w-24 md:w-36"
-              width={140}
-              height={100}
+              width={150}
+              height={150}
             />
           </li>
 
           <li>
             <Image
-              src="/images/ambassadeurs/bfntv.png"
-              alt="BFM TV"
-              className="h-auto w-12 md:w-20"
-              width={80}
+              src="/images/ambassadeurs/france_inter.png"
+              alt="France Inter"
+              className="h-auto w-16 md:w-24"
+              width={100}
               height={100}
             />
           </li>
 
           <li>
             <Image
-              src="/images/ambassadeurs/vert_rectangulaire.png"
-              alt="Vert Media"
-              className="h-auto w-20 md:w-32"
-              width={120}
+              src="https://nosgestesclimat-prod.s3.fr-par.scw.cloud/cms/strasbourg_d05212223e.jpg"
+              alt="Strasbourg Métropole"
+              className="h-auto w-24 md:w-40"
+              width={100}
               height={100}
             />
           </li>
 
           <li>
             <Image
-              src="/images/ambassadeurs/challenges.png"
-              alt="Challenges"
-              className="h-auto w-28 md:w-40"
-              width={160}
+              src="/images/ambassadeurs/nantes metropole.jpg"
+              alt="Nantes Métropole"
+              className="h-auto w-16 md:w-24"
+              width={100}
               height={100}
             />
           </li>
         </ul>
+
+        <div className="flex justify-center">
+          <ButtonLink color="secondary" size="xl" href="/nos-relais">
+            <TransServer locale={locale}>Rejoignez-les</TransServer>
path: src/app/[locale]/_components/TheySpeakAboutUs.tsx
user: login: bjlaa
body: Don't use TransServer but a  Trans alias so translation is detected by script
created_at: 2025-04-14T12:24:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,54 +50,60 @@ export default function TheySpeakAboutUs({ locale }: { locale: string }) {
 
           <li>
             <Image
-              src="/images/ambassadeurs/france_inter.png"
-              alt="France Inter"
-              className="h-auto w-16 md:w-24"
-              width={100}
-              height={100}
+              src="/images/ambassadeurs/leboncoin.png"
+              alt="Leboncoin"
+              className="h-auto w-24 md:w-36"
+              width={150}
+              height={150}
             />
           </li>
 
           <li>
             <Image
-              src="/images/ambassadeurs/bon-pote-no-background.svg"
-              alt="Bon Pote"
+              src="/images/ambassadeurs/france_info.png"
+              alt="France Info"
               className="h-auto w-24 md:w-36"
-              width={140}
-              height={100}
+              width={150}
+              height={150}
             />
           </li>
 
           <li>
             <Image
-              src="/images/ambassadeurs/bfntv.png"
path: src/app/[locale]/_components/TheySpeakAboutUs.tsx
user: login: paulsouche
body: Remove if unused
created_at: 2025-04-14T12:27:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,6 +28,13 @@ import Logo from '../misc/Logo'
 import LanguageSwitchButton from '../translation/LanguageSwitchButton'
 import Trans from '../translation/trans/TransClient'
 
+const WHITE_BACKGROUND_PATHS = [
+  '/empreinte-eau',
+  '/empreinte-carbone',
+  '/blog',
+  '/organisations',
+]
+
path: src/components/layout/Footer.tsx
user: login: paulsouche
body: ```suggestion
const WHITE_BACKGROUND_PATHS = new Set([
  '/empreinte-eau',
  '/empreinte-carbone',
  '/blog',
  '/organisations',
])
```
created_at: 2025-04-14T12:29:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -39,9 +46,7 @@ export default function Footer({ className = '' }) {
   const shouldUseWhiteBackground =
     pathname === '/' ||
     pathname === `/${locale}` ||
-    pathname.includes('/empreinte-eau') ||
-    pathname.includes('/empreinte-carbone') ||
-    pathname.includes('/blog')
+    WHITE_BACKGROUND_PATHS.includes(pathname)
path: src/components/layout/Footer.tsx
user: login: paulsouche
body: ```suggestion
    WHITE_BACKGROUND_PATHS.has(pathname)
```
created_at: 2025-04-14T12:29:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,78 @@
+'use client'
+
+import { FILTER_SEARCH_PARAM_KEY } from '@/constants/filtering'
+import { trackingCategoryFilter } from '@/constants/tracking/misc'
+import {
+  getBackgroundColor,
+  getBackgroundLightColor,
+  getTextDarkColor,
+} from '@/helpers/getCategoryColorClass'
+import { trackEvent } from '@/utils/analytics/trackEvent'
+import { encodeDottedNameAsURI } from '@/utils/format/encodeDottedNameAsURI'
+import type { DottedName } from '@incubateur-ademe/nosgestesclimat'
+import { useRouter, useSearchParams } from 'next/navigation'
+
+type Props = {
+  title: string
+  dottedName: DottedName
+  count: number
+}
+
+export default function CategoryFilter({ title, dottedName, count }: Props) {
+  const router = useRouter()
+
+  const categorySelected = useSearchParams().get(FILTER_SEARCH_PARAM_KEY) || ''
+
+  const encodedDottedName = encodeDottedNameAsURI(dottedName)
+
+  const isSelected = categorySelected === encodedDottedName
+
+  const buildURL = () => {
+    const siteURL = new URL(
+      `${window.location.origin}${window.location.pathname}`
+    )
+
+    if (isSelected && encodedDottedName === categorySelected) {
+      siteURL.searchParams.delete(FILTER_SEARCH_PARAM_KEY)
+    }
+
+    if (isSelected && encodedDottedName !== categorySelected) {
+      siteURL.searchParams.set(FILTER_SEARCH_PARAM_KEY, encodedDottedName)
+    }
+
+    if (!isSelected) {
+      siteURL.searchParams.append(FILTER_SEARCH_PARAM_KEY, encodedDottedName)
+    }
+
+    return siteURL.toString()
+  }
+
+  return (
+    <li
+      className={`height-[1.8rem] rounded-md ${
+        !categorySelected || isSelected
+          ? getBackgroundLightColor(dottedName)
+          : 'bg-gray-200'
+      }`}
+      style={{
+        backgroundColor: getBackgroundColor(),
+      }}>
+      <button
+        className={`p-2 text-xs font-bold ${getTextDarkColor(dottedName)}`}
+        onClick={() => {
+          trackEvent(
+            trackingCategoryFilter(dottedName, window.location.pathname)
+          )
+          router.replace(buildURL(), {
+            scroll: false,
+          })
path: src/components/filtering/categoryFilters/CategoryFilter.tsx
user: login: paulsouche
body: Could it be improved ?
created_at: 2025-04-14T12:32:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: notion-workspace[bot]
created_at: 2025-04-02T10:35:03Z
body: <a href="https://www.notion.so/Mettre-jour-la-page-relais-partenaires-1ad6523d57d7800c85acd5059a7b8b2c?pvs=8&n=github_linkback">Mettre à jour la page relais/partenaires</a>
author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-02T10:35:04Z
body: [vc]: #cw0Cu2xROv1z01EpJvJUcVekXsOSV8qjZntUNDu7J+8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJub3NnZXN0ZXNjbGltYXQiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYWRlbWUvbm9zZ2VzdGVzY2xpbWF0LzhvR0R1R21WWjdBdTZRV3VZQ3g0MVlURnRpZXMiLCJwcmV2aWV3VXJsIjoibm9zZ2VzdGVzY2xpbWF0LWdpdC1uZ2MtMTcxOS1hZGVtZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicm9vdERpcmVjdG9yeSI6bnVsbH1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **nosgestesclimat** | ✅ Ready ([Inspect](https://vercel.com/ademe/nosgestesclimat/8oGDuGmVZ7Au6QWuYCx41YTFties)) | [Visit Preview](https://nosgestesclimat-git-ngc-1719-ademe.vercel.app) | Apr 15, 2025 8:20am |


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-02T10:36:41Z
body: <!-- bot: yarn-compile-result -->

<h1 align=center>Report for the pull request #908</h1>

---

### :globe_with_meridians: Translation status

#### UI's texts

| Language | Nb. missing translations | Status |
|:--------:|:-------------------------|:------:|
| _en_ | <details><summary>Missing 3 UI texts :arrow_down:</summary><ul>N.B. : aucun acteur cité ci-dessous ne finance Nos Gestes Climat, qui est et restera un service public, indépendant et gratuit de l'ADEME.,N.B. : aucun acteur cité ci-dessous ne finance Nos Gestes Climat, qui est et restera un service public, indépendant et gratuit de l'ADEME. Plusieurs milliers d’organisations nous font confiance pour sensibiliser efficacement,Plusieurs milliers d’organisations nous font confiance pour sensibiliser efficacement Rejoignez-les,Rejoignez-les</ul></details> | :x: |
| _es_ | <details><summary>Missing 3 UI texts :arrow_down:</summary><ul>N.B. : aucun acteur cité ci-dessous ne finance Nos Gestes Climat, qui est et restera un service public, indépendant et gratuit de l'ADEME.,N.B. : aucun acteur cité ci-dessous ne finance Nos Gestes Climat, qui est et restera un service public, indépendant et gratuit de l'ADEME. Plusieurs milliers d’organisations nous font confiance pour sensibiliser efficacement,Plusieurs milliers d’organisations nous font confiance pour sensibiliser efficacement Rejoignez-les,Rejoignez-les</ul></details> | :x: |

#### FAQ's questions

| Language | Nb. missing translations | Status |
|:--------:|:-------------------------|:------:|
| _en_ | Ø | :heavy_check_mark: |
| _es_ | Ø | :heavy_check_mark: |

> _You will find more information about the translation in the dedicated file._


author_association: NONE