├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitconfig ├── .github ├── ISSUE_TEMPLATE │ └── -r-flexion-ou-contribution-.md ├── actions │ ├── install │ │ └── action.yaml │ └── reopen-issue-with-comment │ │ ├── action.yaml │ │ └── main.mjs └── workflows │ ├── check-links-validity.yaml │ ├── pr-updater.yaml │ ├── publish-wiki.yaml │ ├── test-e2e.yaml │ ├── update-submodule.yaml │ └── upload-translation-status.yaml ├── .gitignore ├── .gitmodules ├── .gitpod.yml ├── .nvmrc ├── .prettierignore ├── .prettierrc.yaml ├── .vscode ├── extensions.json └── settings.json ├── .yarnclean ├── 4dc3300c431ca82c00785768559ea871.html ├── CONTRIBUTING.md ├── FICHEPRODUIT.md ├── LICENSE ├── PERSONNALISATION.md ├── README.md ├── babel.config.js ├── bundlesize.config.json ├── cypress.config.js ├── cypress ├── e2e │ ├── integration │ │ ├── about.cy.js │ │ ├── accessibilite.cy.js │ │ ├── group.cy.js │ │ ├── homepage.cy.js │ │ ├── news.cy.js │ │ └── personas.cy.js │ ├── pages │ │ ├── groupes.cy.js │ │ └── simulation.cy.js │ ├── test-completion │ │ ├── default-FR-fr.cy.js │ │ ├── default.cy.js │ │ ├── questions-order.cy.js │ │ └── url-redirection.cy.js │ └── utils.js ├── scripts │ └── generateSpecsFromPersonas.js └── support │ └── e2e.ts ├── dist ├── demo-iframe.html └── demo-iframeSimulation.html ├── docs ├── Contributing.md ├── Customisation.md ├── Documentation.md ├── Home.md ├── Publicodes.md ├── Translation.md ├── _Footer.md ├── _Sidebar.md └── mosaic.md ├── index.html ├── jest.config.js ├── loaders └── locale-yaml-loader.js ├── manifest.webmanifest ├── netlify.toml ├── netlify └── functions │ ├── countries.json │ ├── create-issue.js │ ├── decrypt-data.ts │ ├── email-service.ts │ ├── encrypt-data.ts │ ├── ending-screenshot.js │ ├── geolocation.js │ ├── get-newsletter-subscribers-number.ts │ └── group-email-service.ts ├── package.json ├── postcss.config.js ├── scripts ├── .eslintrc.yaml ├── check-links-validity.mjs ├── fetch-releases.ts ├── generateSitemap.ts └── i18n │ ├── check-faq.js │ ├── check-ui.js │ ├── generate-ui.js │ ├── parser.config.js │ ├── paths.js │ ├── translate-faq.js │ ├── translate-pages.js │ ├── translate-release.js │ └── translate-ui.js ├── source ├── AnimatedLoader.tsx ├── Provider.tsx ├── RulesProvider.tsx ├── actions │ └── actions.ts ├── analytics │ └── matomo-events.ts ├── components │ ├── AnimatedIllustration.tsx │ ├── CardGameIcon.tsx │ ├── CircledEmojis.tsx │ ├── CurrencyInput │ │ ├── CurrencyInput.css │ │ ├── CurrencyInput.test.js │ │ └── CurrencyInput.tsx │ ├── EngineValue.tsx │ ├── ErrorFallback.tsx │ ├── Feedback │ │ ├── Feedback.css │ │ ├── FeedbackForm.tsx │ │ ├── LinkToForm.tsx │ │ ├── Northstar.tsx │ │ ├── NorthstarBanner.tsx │ │ ├── PageFeedback.tsx │ │ └── about.md │ ├── Footer.tsx │ ├── IframeResizer.tsx │ ├── IllustratedButton.tsx │ ├── IllustrationSVG.tsx │ ├── LandingLayout.tsx │ ├── LangSwitcher.tsx │ ├── Logo.tsx │ ├── Modal.tsx │ ├── NewsBanner.tsx │ ├── NotificationBubble.tsx │ ├── Notifications.css │ ├── Notifications.tsx │ ├── PartnerBanner.tsx │ ├── PercentageField.css │ ├── PercentageField.tsx │ ├── PeriodSwitch.css │ ├── PeriodSwitch.tsx │ ├── ProgressCircle.tsx │ ├── Route404.tsx │ ├── RuleLink.tsx │ ├── SafeCategoryImage.tsx │ ├── ScoreExplanation.tsx │ ├── SearchBar.css │ ├── SearchBar.tsx │ ├── SearchBar.worker.js │ ├── SearchButton.tsx │ ├── SessionBar.tsx │ ├── ShareButton.tsx │ ├── ShareButtonIcon.tsx │ ├── Simulation.tsx │ ├── SlidesLayout.tsx │ ├── TranslationAlertBanner.tsx │ ├── conversation │ │ ├── Aide.css │ │ ├── Aide.tsx │ │ ├── AnswerList.css │ │ ├── AnswerList.tsx │ │ ├── CategoryRespiration.tsx │ │ ├── Conversation.tsx │ │ ├── DateInput.tsx │ │ ├── Explicable.css │ │ ├── Explicable.tsx │ │ ├── Input.tsx │ │ ├── InputEstimation.tsx │ │ ├── InputSuggestions.tsx │ │ ├── MosaicInputSuggestions.tsx │ │ ├── ParagrapheInput.tsx │ │ ├── Question.tsx │ │ ├── QuestionFinder.tsx │ │ ├── QuestionFinder.worker.ts │ │ ├── QuestionFinderWrapper.tsx │ │ ├── RuleInput.tsx │ │ ├── SeeAnswersButton.tsx │ │ ├── SimulationEnding.tsx │ │ ├── TextInput.tsx │ │ ├── UI.js │ │ ├── amortissement-avion │ │ │ ├── Amortissement.tsx │ │ │ ├── AmortissementButton.tsx │ │ │ ├── FieldTravelDuration.tsx │ │ │ └── Form.tsx │ │ ├── conversation.css │ │ ├── conversationUtils.ts │ │ ├── estimate │ │ │ ├── AnswerTrajetsTable.js │ │ │ ├── KmEstimation.tsx │ │ │ └── KmHelp │ │ │ │ ├── EditableRow.js │ │ │ │ ├── KmForm.tsx │ │ │ │ ├── KmHelpButton.js │ │ │ │ ├── KmInput.tsx │ │ │ │ ├── ReadOnlyRow.js │ │ │ │ ├── dataHelp.js │ │ │ │ └── index.tsx │ │ ├── estimationQuestions.tsx │ │ └── select │ │ │ ├── MosaicStamp.tsx │ │ │ ├── NumberedMosaic.tsx │ │ │ ├── SelectDevices.tsx │ │ │ └── UI.js │ ├── emailing │ │ └── NewsletterForm.tsx │ ├── emoji.tsx │ ├── groupe │ │ ├── Button.tsx │ │ ├── ButtonLink.tsx │ │ ├── Container.tsx │ │ ├── CopyInput.tsx │ │ ├── EmailInput.tsx │ │ ├── GoBackLink.tsx │ │ ├── InlineTextInput.tsx │ │ ├── Link.tsx │ │ ├── PrenomInput.tsx │ │ ├── Separator.tsx │ │ ├── TextInputGroup.tsx │ │ └── Title.tsx │ ├── highlightMatches.tsx │ ├── icons │ │ └── ChevronRight.tsx │ ├── images │ │ └── LogoMIT.tsx │ ├── localisation │ │ ├── CountryFlag.tsx │ │ ├── Localisation.tsx │ │ ├── LocalisationMessage.tsx │ │ ├── LocalisationProvider.tsx │ │ ├── RegionGrid.tsx │ │ ├── RegionModelAuthors.tsx │ │ ├── RegionSelector.tsx │ │ ├── frenchCountryPrepositions.yaml │ │ ├── useCurrentRegionCode.ts │ │ ├── useLocalisation.ts │ │ ├── useOrderedSupportedRegions.ts │ │ └── utils.ts │ ├── publicodesUtils.tsx │ ├── stats │ │ ├── StatsContent.js │ │ ├── content │ │ │ ├── Chart.tsx │ │ │ ├── DurationChart.js │ │ │ ├── DurationFigures.js │ │ │ ├── Evolution.js │ │ │ ├── IframeFigures.js │ │ │ ├── KmFigures.js │ │ │ ├── ScoreFromURL.js │ │ │ ├── Sources.js │ │ │ ├── TotalChart.js │ │ │ ├── chart │ │ │ │ ├── CustomTooltip.js │ │ │ │ └── Search.js │ │ │ ├── histogram │ │ │ │ └── CustomTooltip.js │ │ │ └── sources │ │ │ │ └── Table.js │ │ ├── matomo.js │ │ └── utils │ │ │ ├── FancySelect.js │ │ │ ├── Section.js │ │ │ └── Tile.js │ ├── ui │ │ ├── AnimatedTargetValue.tsx │ │ ├── Button │ │ │ ├── button.css │ │ │ └── index.tsx │ │ ├── Card.css │ │ ├── Checkbox │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── Fonts.css │ │ ├── IllustratedMessage.tsx │ │ ├── InfoBulle.css │ │ ├── InfoBulle.tsx │ │ ├── NeutralH1.tsx │ │ ├── Progress.css │ │ ├── Progress.tsx │ │ ├── RangeSlider.tsx │ │ ├── SocialIcon.tsx │ │ ├── Toggle.css │ │ ├── ToggleSwitch.tsx │ │ ├── Typography.css │ │ ├── WarningBlock.tsx │ │ ├── animate.tsx │ │ ├── fonts │ │ │ ├── Marianne-Bold.woff │ │ │ ├── Marianne-Bold.woff2 │ │ │ ├── Marianne-ExtraBold.woff │ │ │ ├── Marianne-ExtraBold.woff2 │ │ │ ├── Marianne-Light.woff │ │ │ ├── Marianne-Light.woff2 │ │ │ ├── Marianne-Medium.woff │ │ │ ├── Marianne-Medium.woff2 │ │ │ ├── Marianne-Regular.woff │ │ │ ├── Marianne-Regular.woff2 │ │ │ ├── Marianne-Thin.woff │ │ │ └── Marianne-Thin.woff2 │ │ ├── index.css │ │ └── reset.css │ ├── useBranchData.ts │ ├── useFetchDocumentation.ts │ └── utils │ │ ├── AutoCanonicalTag.tsx │ │ ├── DisableScroll.ts │ │ ├── Emoji.tsx │ │ ├── EngineContext.tsx │ │ ├── IframeOptionsProvider.tsx │ │ ├── Meta.tsx │ │ ├── NewTabSvg.tsx │ │ ├── Scroll.tsx │ │ ├── SitePathsContext.tsx │ │ ├── colors.tsx │ │ ├── embeddedContext.js │ │ ├── embeddedContext.ts │ │ ├── formatDataForDB.ts │ │ ├── formatFloat.ts │ │ ├── index.tsx │ │ ├── markdown.tsx │ │ └── toCSV.ts ├── constants │ ├── groupNames.ts │ └── urls.ts ├── contexts │ └── MatomoContext.tsx ├── global.css ├── hooks │ ├── useDisplayOnIntersecting.ts │ ├── useEventListener.ts │ ├── useGetCurrentSimulation.ts │ ├── useKeyPress.ts │ ├── useLoadSimulationFromURL.ts │ ├── useMediaQuery.tsx │ ├── useNextQuestion.tsx │ ├── usePersistState.ts │ └── useSetUserId.ts ├── images │ ├── 1F1FA-1F1E6.svg │ ├── 1F3DF.svg │ ├── 1F447.svg │ ├── 1F464.svg │ ├── 1F465.svg │ ├── 1F4BE.svg │ ├── 1F4C5.svg │ ├── 1F4CA.svg │ ├── 1F50D.svg │ ├── 2699.svg │ ├── 26D4.svg │ ├── 270A.svg │ ├── 270F.svg │ ├── 2714.svg │ ├── 274C.svg │ ├── 2754.svg │ ├── ABC.svg │ ├── CO2e.tsx │ ├── E045.svg │ ├── E10C.svg │ ├── E262.svg │ ├── Logo.tsx │ ├── Printemps-pour-la-planete.svg │ ├── abacus-france.svg │ ├── alexandre-lecocq-climatiseurs.jpg │ ├── arrow.svg │ ├── burger-menu.svg │ ├── climate-change-small.svg │ ├── climate-change-small.variations.svg │ ├── close-plain.svg │ ├── co2.svg │ ├── co2e.svg │ ├── dessin-nosgestesclimat.png │ ├── ecolab-climat-dessin.clipped-test.svg │ ├── ecolab-climat-dessin.png │ ├── electricitymaps.svg │ ├── exemple-contexte.png │ ├── favicon.png │ ├── filtre.svg │ ├── fullscreen.svg │ ├── glowing-ngc-star.svg │ ├── greenhouse-effect.svg │ ├── illustration-micmac.png │ ├── illustration.svg │ ├── info.svg │ ├── international-illustration.jpeg │ ├── jeu-de-cartes.svg │ ├── logo-france-relance.svg │ ├── logo.png │ ├── logo.svg │ ├── logoADEME.svg │ ├── map-directions.png │ ├── marianne.svg │ ├── matt-benson-chien-baignade.jpg │ ├── methane.svg │ ├── model │ │ ├── alimentation . boisson.svg │ │ ├── alimentation . déchets.svg │ │ ├── alimentation . déjeuner et dîner.svg │ │ ├── alimentation . gaspillage alimentaire.svg │ │ ├── alimentation . petit déjeuner annuel.svg │ │ ├── alimentation . plats.svg │ │ ├── alimentation . repas.svg │ │ ├── alimentation.svg │ │ ├── bilan.svg │ │ ├── divers . ameublement.svg │ │ ├── divers . animaux domestiques.svg │ │ ├── divers . autres produits.svg │ │ ├── divers . loisirs . culture.svg │ │ ├── divers . loisirs . sports.svg │ │ ├── divers . loisirs.svg │ │ ├── divers . numérique . internet.svg │ │ ├── divers . numérique . ordinateur fixe.svg │ │ ├── divers . numérique . ordinateur portable.svg │ │ ├── divers . numérique . tablette.svg │ │ ├── divers . numérique . téléphone.svg │ │ ├── divers . numérique.svg │ │ ├── divers . tabac.svg │ │ ├── divers . textile.svg │ │ ├── divers . électroménager.svg │ │ ├── divers.svg │ │ ├── empreinte branche . C30 par hab . services marchands et sociétaux.svg │ │ ├── empreinte branche . F42 par hab . services publics.svg │ │ ├── empreinte branche . F43 par hab . services publics.svg │ │ ├── empreinte branche . J61 par hab . services marchands et sociétaux.svg │ │ ├── empreinte branche . M72 par hab . services marchands et sociétaux.svg │ │ ├── empreinte branche . M72 par hab . services publics.svg │ │ ├── empreinte branche . O84 par hab . services publics.svg │ │ ├── empreinte branche . P85 par hab . services publics.svg │ │ ├── empreinte branche . Q86 par hab . services publics.svg │ │ ├── logement . chauffage . empreinte par défaut.svg │ │ ├── logement . chauffage.svg │ │ ├── logement . climatisation.svg │ │ ├── logement . construction.svg │ │ ├── logement . piscine . empreinte.svg │ │ ├── logement . électricité.svg │ │ ├── logement.svg │ │ ├── maison.svg │ │ ├── numérique . TV.svg │ │ ├── services marchands.svg │ │ ├── services publics.svg │ │ ├── services sociétaux.svg │ │ ├── transport . avion.svg │ │ ├── transport . bus.svg │ │ ├── transport . deux roues thermique.svg │ │ ├── transport . ferry.svg │ │ ├── transport . train.svg │ │ ├── transport . vacances.svg │ │ ├── transport . voiture . empreinte.svg │ │ ├── transport . voiture.svg │ │ └── transport.svg │ ├── n2o.svg │ ├── nosgestesclimat.webp │ ├── objectif-climat.svg │ ├── pencil.svg │ ├── petit-logo.png │ ├── petit-logo@2x.png │ ├── petit-logo@3x.png │ ├── pexels-helena-lopes-4409455.jpg │ ├── pompe-essence.svg │ ├── priscilla-du-preez-rollercoaster.jpg │ ├── publicodes.png │ ├── share.svg │ ├── silhouette.svg │ ├── silhouettes.svg │ ├── ted-bryan-yu-montagnes.jpg │ ├── thin-arrow-left.svg │ ├── three-dots.svg │ ├── transparent.png │ ├── triangle.svg │ ├── union-européenne.svg │ └── william-bossen-fonte-glaces.jpg ├── locales │ ├── .gitignore │ ├── blog │ │ └── fr │ │ │ ├── budget.md │ │ │ ├── campus.md │ │ │ ├── effet-rebond.md │ │ │ ├── feuille-route.md │ │ │ ├── gesTransport.md │ │ │ ├── historique.md │ │ │ ├── maladaptation.md │ │ │ ├── mobilite.md │ │ │ └── mondialEnvironnement.md │ ├── faq │ │ ├── FAQ-en.yaml │ │ ├── FAQ-es.yaml │ │ ├── FAQ-fr.yaml │ │ └── FAQ-it.yaml │ ├── i18n.ts │ ├── pages │ │ ├── en │ │ │ ├── CGU.md │ │ │ ├── about.mdx │ │ │ ├── accessibility.md │ │ │ ├── budgetBottom.md │ │ │ ├── budgetTop.md │ │ │ ├── diffuser.md │ │ │ ├── documentation.md │ │ │ ├── landing.md │ │ │ ├── méthode.md │ │ │ ├── petrogazLanding.md │ │ │ └── privacy.md │ │ ├── es │ │ │ ├── CGU.md │ │ │ ├── about.md │ │ │ ├── accessibility.md │ │ │ ├── diffuser.md │ │ │ ├── documentation.md │ │ │ ├── landing.md │ │ │ ├── méthode.md │ │ │ └── privacy.md │ │ ├── fr │ │ │ ├── CGU.md │ │ │ ├── about.mdx │ │ │ ├── accessibility.md │ │ │ ├── budgetBottom.md │ │ │ ├── budgetTop.md │ │ │ ├── diffuser.md │ │ │ ├── documentation.md │ │ │ ├── landing.md │ │ │ ├── méthode.md │ │ │ ├── petrogazLanding.md │ │ │ └── privacy.md │ │ └── it │ │ │ ├── CGU.md │ │ │ ├── about.md │ │ │ ├── accessibility.md │ │ │ ├── diffuser.md │ │ │ ├── documentation.md │ │ │ ├── landing.md │ │ │ ├── méthode.md │ │ │ └── privacy.md │ ├── releases │ │ ├── releases-en.json │ │ ├── releases-es.json │ │ ├── releases-fr.json │ │ └── releases-it.json │ ├── translation.ts │ ├── ui │ │ ├── ui-en.yaml │ │ ├── ui-es.yaml │ │ ├── ui-fr.yaml │ │ └── ui-it.yaml │ └── units.yaml ├── légal │ ├── cgu.docx │ └── confidentialité.docx ├── pages │ ├── creer-groupe │ │ └── index.tsx │ ├── groupe-dashboard │ │ ├── components │ │ │ ├── Badge.tsx │ │ │ ├── Classement.tsx │ │ │ ├── ClassementMember.tsx │ │ │ ├── FeedbackBlock.tsx │ │ │ ├── Footer.tsx │ │ │ ├── InviteBlock.tsx │ │ │ ├── PercentageDiff.tsx │ │ │ ├── PointsFortsFaibles.tsx │ │ │ ├── SondagesBlock.tsx │ │ │ └── VotreEmpreinte.tsx │ │ ├── hooks │ │ │ └── useGetGroupStats.ts │ │ ├── index.tsx │ │ └── utils │ │ │ └── getTopThreeAndRestMembers.ts │ ├── mes-groupes │ │ ├── components │ │ │ ├── CreateFirstGroupSection.tsx │ │ │ ├── CreateOtherGroupsSection.tsx │ │ │ ├── GroupItem.tsx │ │ │ ├── GroupList.tsx │ │ │ └── ServerErrorSection.tsx │ │ └── index.tsx │ ├── rejoindre-groupe │ │ └── index.tsx │ └── supprimer-groupe │ │ └── index.tsx ├── reducers │ ├── group │ │ └── index.ts │ ├── rootReducer.ts │ ├── storageReducer.ts │ └── user │ │ └── index.ts ├── selectors │ ├── groupSelectors.ts │ ├── simulationSelectors.ts │ └── storageSelectors.ts ├── sites │ └── publicodes │ │ ├── Action.tsx │ │ ├── ActionCard.tsx │ │ ├── ActionConversation.tsx │ │ ├── ActionPlus.js │ │ ├── ActionStack.tsx │ │ ├── ActionTutorial.tsx │ │ ├── ActionVignette.tsx │ │ ├── Actions.tsx │ │ ├── ActionsChosenIndicator.tsx │ │ ├── ActionsList.tsx │ │ ├── ActionsOptionsBar.tsx │ │ ├── AllActions.tsx │ │ ├── App.tsx │ │ ├── BandeauContribuer.js │ │ ├── CategoryFilters.tsx │ │ ├── CategoryVisualisation.tsx │ │ ├── Contact.tsx │ │ ├── DefaultFootprint.tsx │ │ ├── DocumentationButton.js │ │ ├── DocumentationReferences.tsx │ │ ├── FAQ.js │ │ ├── HorizontalSwipe.tsx │ │ ├── HumanWeight.js │ │ ├── Landing.tsx │ │ ├── LandingContent.tsx │ │ ├── LandingExplanations.tsx │ │ ├── ListeActionPlus.js │ │ ├── Logo.js │ │ ├── MetricFilters.tsx │ │ ├── ModeChoice.tsx │ │ ├── Model.tsx │ │ ├── ModelDemoBlock.tsx │ │ ├── ModelIssuePreviews.tsx │ │ ├── ModelStatsBlock.tsx │ │ ├── Navigation.tsx │ │ ├── Personas.tsx │ │ ├── PetrolScore.tsx │ │ ├── Profil.tsx │ │ ├── ScoreBar.tsx │ │ ├── Search.js │ │ ├── Simulateur.tsx │ │ ├── SimulationList.tsx │ │ ├── SimulationMissing.tsx │ │ ├── SkipLinks.tsx │ │ ├── StoreContext.js │ │ ├── SurveyModal.js │ │ ├── TranslationContribution.tsx │ │ ├── avantages.yaml │ │ ├── catégories.js │ │ ├── chart │ │ ├── Bar.js │ │ ├── DetailedBarChartIcon.tsx │ │ ├── GridChart.tsx │ │ ├── Inhabitants.tsx │ │ ├── InlineCategoryChart.tsx │ │ ├── RavijenChart.tsx │ │ ├── SpecializedVisualisation.tsx │ │ ├── SquaresGrid.tsx │ │ ├── SubCategoriesChart.tsx │ │ ├── SubCategoryBar.tsx │ │ ├── TriangleShape.tsx │ │ ├── Value.js │ │ ├── chartUtils.ts │ │ ├── index.js │ │ └── useContinuousCategory.tsx │ │ ├── chrono.js │ │ ├── chrono.yaml │ │ ├── conference │ │ ├── CategoryStats.tsx │ │ ├── Conference.tsx │ │ ├── ConferenceBar.tsx │ │ ├── ConferenceBarLazy.tsx │ │ ├── ContextConversation.tsx │ │ ├── DataWarning.tsx │ │ ├── FilterBar.tsx │ │ ├── GroupModeSessionVignette.tsx │ │ ├── GroupStats.tsx │ │ ├── GroupSwitch.tsx │ │ ├── Instructions.tsx │ │ ├── LoadingButton.tsx │ │ ├── NamingBlock.tsx │ │ ├── NoSurveyCreatedWarning.tsx │ │ ├── NoTestMessage.tsx │ │ ├── Survey.tsx │ │ ├── SurveyBar.tsx │ │ ├── SurveyBarLazy.tsx │ │ ├── UserList.tsx │ │ ├── adjectifs.json │ │ ├── conferenceStyle.tsx │ │ ├── fruits.json │ │ ├── participePassés.json │ │ ├── périodesGéologiques.json │ │ ├── useDatabase.tsx │ │ ├── useYjs.tsx │ │ └── utils.tsx │ │ ├── design-graphique.svg │ │ ├── enquête │ │ ├── Banner.tsx │ │ ├── BannerContent.tsx │ │ ├── BannerWrapper.tsx │ │ ├── Enquête.tsx │ │ ├── ReturnToEnquêteButton.tsx │ │ ├── SendResultButton.tsx │ │ ├── enquêteSelector.ts │ │ └── texte.md │ │ ├── entry.js │ │ ├── fin │ │ ├── ActionSlide.tsx │ │ ├── ActionTeaser.tsx │ │ ├── Budget.tsx │ │ ├── Buttons.tsx │ │ ├── Catégories.tsx │ │ ├── ClimateTargetChart.tsx │ │ ├── FinShareButton.tsx │ │ ├── IframeDataShareModal.tsx │ │ ├── Petrogaz.tsx │ │ ├── ballonGES.svg │ │ └── index.tsx │ │ ├── iframe.js │ │ ├── iframeSimulation.js │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── pages │ │ ├── About.tsx │ │ ├── Accessibility.tsx │ │ ├── Blog.tsx │ │ ├── BlogArticle.tsx │ │ ├── BlogData.tsx │ │ ├── Budget.tsx │ │ ├── CGU.tsx │ │ ├── Diffuser.tsx │ │ ├── Documentation.tsx │ │ ├── DocumentationContexte.tsx │ │ ├── DocumentationLanding.tsx │ │ ├── DocumentationPage.tsx │ │ ├── DocumentationStyle.tsx │ │ ├── FriendlyObjectViewer.tsx │ │ ├── GuideGroupe.js │ │ ├── International.tsx │ │ ├── MarkdownPage.tsx │ │ ├── MarkdownXPage.tsx │ │ ├── Méthode.tsx │ │ ├── NorthstarStats.tsx │ │ ├── PetrogazLanding.tsx │ │ ├── Plan.tsx │ │ ├── Privacy.tsx │ │ ├── QuestionList.tsx │ │ ├── QuickDocumentationPage.tsx │ │ ├── Stats.js │ │ ├── budget │ │ │ ├── RessourcesAllocationTable.js │ │ │ ├── SelectYear.js │ │ │ └── budget.yaml │ │ ├── editorialisedModels.yaml │ │ └── news │ │ │ ├── News.tsx │ │ │ ├── NewsItem.tsx │ │ │ └── NewsList.tsx │ │ ├── personas │ │ ├── RawActionsList.tsx │ │ ├── RulesCompletion.tsx │ │ ├── Summary.tsx │ │ └── personasUtils.ts │ │ ├── questionConfig.js │ │ ├── reducers.js │ │ ├── robots.txt │ │ ├── sitePaths.js │ │ ├── sitemap.txt │ │ ├── tutorial │ │ ├── Categories.tsx │ │ ├── ClimateWarming.tsx │ │ ├── Instructions.tsx │ │ ├── Target.tsx │ │ ├── Tutorial.tsx │ │ ├── TutorialSlide.tsx │ │ └── WarmingMeasure.tsx │ │ ├── useActions.tsx │ │ └── utils.tsx ├── storage │ ├── persistEverything.ts │ ├── persistSimulation.ts │ └── safeLocalStorage.ts ├── types │ ├── .gitignore │ ├── app-env.d.ts │ ├── css-prop.d.ts │ ├── groups.ts │ ├── iframe-resizer.d.ts │ ├── rating.ts │ ├── simulation.ts │ ├── values.ts │ └── worker-loader.d.ts ├── utils.ts └── utils │ ├── fetchAddUserToGroup.ts │ ├── fetchGroup.ts │ ├── fetchUpdateGroupMember.ts │ ├── getIsSimulationValid.ts │ └── getSimulationResults.ts ├── tailwind.config.js ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | # We shall not define indent_size ⬇️ when using tabs. 8 | # tab_width doesn't make much sense as it can be left to the reader to decide. 9 | indent_style = tab 10 | insert_final_newline = true 11 | 12 | [**.{js,jsx,ts,tsx}] 13 | indent_size = 2 14 | max_line_length = 80 15 | 16 | 17 | [**.{yml,yaml}] 18 | # Spaces are mandatory for yaml files: 19 | indent_style = space 20 | indent_size = 2 21 | # A high max_line_length is needed as prettier doesn't manage property-name 22 | # line-wrapping correctly: 23 | # See https://github.com/prettier/prettier/issues/5599 24 | max_line_length = 1000 25 | trim_trailing_whitespace = false 26 | 27 | [*.md] 28 | trim_trailing_whitespace = false 29 | indent_style = space 30 | indent_size = 4 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | # Required because some file names use UTF-8 characters 2 | [core] 3 | quotepath = false 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-r-flexion-ou-contribution-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: " Réflexion ou contribution " 3 | about: Apportez votre contribution au site nosgestesclimat.fr 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Votre idée ou question n'exite-t-elle pas déjà ? 11 | 12 | Il y a presque 200 issues sur [le modèle nosgestesclimat](https://github.com/datagir/nosgestesclimat/issues) et 200 autres ici sur [nosgestesclimat-site](https://github.com/datagir/nosgestesclimat-site/issues). Prenez un peu de temps pour chercher avec quelques mots clefs si votre réflexion n'existe pas déjà. 13 | 14 | ## C'est parti ! 15 | 16 | Si vous n'avez rien trouvé, c'est parti :) 17 | -------------------------------------------------------------------------------- /.github/actions/install/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Install' 2 | description: 'Yarn install with cache' 3 | 4 | runs: 5 | using: 'composite' 6 | steps: 7 | - uses: actions/setup-node@v3 8 | with: 9 | node-version: '18' 10 | cache: 'yarn' 11 | - run: yarn install --immutable 12 | shell: bash 13 | -------------------------------------------------------------------------------- /.github/actions/reopen-issue-with-comment/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Reopen and comment' 2 | description: 'Reopen an issue if closed and add a comment' 3 | inputs: 4 | token: 5 | description: 'GITHUB_TOKEN or a repo scoped PAT.' 6 | default: ${{ github.token }} 7 | issue-number: 8 | description: 'The number of the issue or pull request in which to create a comment.' 9 | comment: 10 | description: 'The comment body.' 11 | runs: 12 | using: 'node16' 13 | main: './main.mjs' 14 | -------------------------------------------------------------------------------- /.github/actions/reopen-issue-with-comment/main.mjs: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | 4 | async function run() { 5 | try { 6 | const inputs = { 7 | token: core.getInput('token'), 8 | issueNumber: Number(core.getInput('issue-number')), 9 | comment: core.getInput('comment'), 10 | } 11 | 12 | const repository = process.env.GITHUB_REPOSITORY 13 | const [owner, repo] = repository.split('/') 14 | 15 | const octokit = github.getOctokit(inputs.token) 16 | 17 | core.info('Re-opening the issue') 18 | await octokit.rest.issues.update({ 19 | owner: owner, 20 | repo: repo, 21 | issue_number: inputs.issueNumber, 22 | state: 'open', 23 | }) 24 | 25 | core.info('Adding a comment') 26 | await octokit.rest.issues.createComment({ 27 | owner: owner, 28 | repo: repo, 29 | issue_number: inputs.issueNumber, 30 | body: inputs.comment.replace(/
/g, `\n`), 31 | }) 32 | } catch (error) { 33 | core.setFailed(error.message) 34 | } 35 | } 36 | 37 | run() 38 | -------------------------------------------------------------------------------- /.github/workflows/check-links-validity.yaml: -------------------------------------------------------------------------------- 1 | name: Check links validity 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # https://crontab.guru/#0_11_*_*_2 6 | - cron: '0 11 * * 2' 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: ./.github/actions/install 13 | - name: Install RipGrep 14 | run: sudo apt install -y ripgrep 15 | - id: invalid_links 16 | run: node ./scripts/check-links-validity.mjs --ci 17 | timeout-minutes: 15 18 | - if: steps.invalid_links.outputs.comment 19 | uses: ./.github/actions/reopen-issue-with-comment 20 | with: 21 | issue-number: 1272 22 | comment: ${{ steps.invalid_links.outputs.comment }} 23 | -------------------------------------------------------------------------------- /.github/workflows/publish-wiki.yaml: -------------------------------------------------------------------------------- 1 | # Updates the repo Wiki from the './docs' folder. 2 | # 3 | name: Documentation 4 | 5 | on: 6 | push: 7 | branches: master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 17 | submodules: recursive 18 | - name: Upload ./docs/ to Wiki 19 | uses: SwiftDocOrg/github-wiki-publish-action@v1 20 | with: 21 | path: "docs" 22 | env: 23 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/update-submodule.yaml: -------------------------------------------------------------------------------- 1 | # From: https://tommoa.me/blog/github-auto-update-submodules/ 2 | 3 | name: Update submodule 4 | 5 | on: 6 | repository_dispatch: 7 | types: update 8 | 9 | jobs: 10 | udpate: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | ref: ${{ github.event.client_payload.ref }} 16 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 17 | submodules: recursive 18 | - name: Update module 19 | run: | 20 | git submodule update --init --recursive --checkout -f --remote -- "${{github.event.client_payload.module}}" 21 | git config --global user.name "GitHub Action" 22 | git config --global user.email "noreply@github.com" 23 | git commit -am "deploy: ${{github.event.client_payload.module}} - ${{github.event.client_payload.sha}}" 24 | git push 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tags* 2 | .tmp 3 | dist/* 4 | !dist/_redirects 5 | !/dist/demo-iframe.html 6 | !/dist/demo-iframeSimulation.html 7 | .DS_Store 8 | yarn-error.log 9 | 10 | package-lock.json 11 | node_modules/ 12 | .env 13 | 14 | # Local Netlify folder 15 | .netlify 16 | 17 | /cypress/videos/* 18 | /cypress/screenshots/* 19 | # They are automatically generated when running cypress in CI 20 | /cypress/e2e/test-completion/persona-*.cy.js 21 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "nosgestesclimat"] 2 | path = nosgestesclimat 3 | url = https://github.com/datagir/nosgestesclimat.git 4 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | additionalRepositories: 2 | - url: https://github.com/datagir/nosgestesclimat 3 | tasks: 4 | - init: yarn 5 | command: yarn start 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/.prettierignore -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | bracketSpacing: true 2 | semi: false 3 | singleQuote: true 4 | pluginSearchDirs: ['./node_modules/'] 5 | plugins: ['prettier-plugin-organize-imports'] 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "ban.spellright", 5 | "jpoissonnier.vscode-styled-components" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "spellright.language": ["fr", "en"], 4 | "spellright.documentTypes": ["yaml", "git-commit", "markdown"], 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "editor.tabSize": 2, 7 | "files.exclude": { 8 | "**/node_modules": true 9 | }, 10 | "eslint.enable": true, 11 | "deno.enable": true, 12 | "deno.enablePaths": ["netlify/edge-functions"], 13 | "deno.unstable": true, 14 | "deno.importMap": ".netlify/edge-functions-import-map.json", 15 | "deno.path": "/Users/benjaminarias/Library/Preferences/netlify/deno-cli/deno", 16 | "editor.defaultFormatter": "esbenp.prettier-vscode", 17 | "[javascript]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[typescript]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[typescriptreact]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode" 25 | }, 26 | "files.associations": { 27 | "*.publicodes": "yaml" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | @types/react-native 2 | -------------------------------------------------------------------------------- /4dc3300c431ca82c00785768559ea871.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 7 | 8 | 9 |

Brevo

10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nosgestesclimat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | [ 12 | '@babel/preset-react', 13 | { 14 | runtime: 'automatic', 15 | }, 16 | ], 17 | '@babel/preset-typescript', 18 | ], 19 | plugins: [ 20 | 'babel-plugin-styled-components', 21 | '@babel/plugin-proposal-class-properties', 22 | '@babel/plugin-proposal-optional-chaining', 23 | '@babel/plugin-proposal-nullish-coalescing-operator', 24 | '@babel/plugin-proposal-object-rest-spread', 25 | '@babel/plugin-syntax-dynamic-import', 26 | ['webpack-alias', { config: './webpack.dev.js' }], 27 | ].filter(Boolean), 28 | } 29 | -------------------------------------------------------------------------------- /bundlesize.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "path": "./dist/*.bundle.js", 5 | "maxSize": "650 kB" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | console.log('CYPRESS_baseUrl', process.env.CYPRESS_baseUrl) 4 | 5 | export default defineConfig({ 6 | projectId: 'dbxhpr', 7 | env: { 8 | // This is the URL of the local server that will be used for testing 9 | personas_fr_url: 'https://data.nosgestesclimat.fr/personas-fr.json', 10 | localisation_param: 'FR', 11 | language_param: 'fr', 12 | }, 13 | e2e: { 14 | baseUrl: process.env.CYPRESS_baseUrl ?? 'http://localhost:8080', 15 | setupNodeEvents(on, config) {}, 16 | experimentalRunAllSpecs: true, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /cypress/e2e/integration/about.cy.js: -------------------------------------------------------------------------------- 1 | describe('check for about page status', () => { 2 | beforeEach(() => { 3 | cy.visit('/à-propos') 4 | }) 5 | 6 | it('has a title', () => { 7 | cy.get('[data-cypress-id="about-us-title"]').should('be.visible') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/integration/accessibilite.cy.js: -------------------------------------------------------------------------------- 1 | describe('check for about page status', () => { 2 | beforeEach(() => { 3 | cy.visit('/accessibilite') 4 | }) 5 | 6 | it('has a title', () => { 7 | cy.get('[data-cypress-id="accessibility-statement-title"]').should('be.visible') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/integration/group.cy.js: -------------------------------------------------------------------------------- 1 | describe('check for group page status', () => { 2 | beforeEach(() => { 3 | cy.visit('/groupe') 4 | }) 5 | 6 | it('has a start button', () => { 7 | cy.get('[data-cypress-id="group-start-button"]').should('be.visible') 8 | }) 9 | it('has a title', () => { 10 | cy.get('[data-cypress-id="group-title"]').should('be.visible') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/integration/homepage.cy.js: -------------------------------------------------------------------------------- 1 | describe('check for homepage status', () => { 2 | beforeEach(() => { 3 | cy.visit('/') 4 | }) 5 | 6 | it('has a start button', () => { 7 | cy.get('[data-cypress-id="do-the-test-link"]').should('be.visible') 8 | }) 9 | it('has a group button', () => { 10 | cy.get('[data-cypress-id="as-a-group-link"]').should('be.visible') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/integration/news.cy.js: -------------------------------------------------------------------------------- 1 | describe('check for about page status', () => { 2 | beforeEach(() => { 3 | cy.visit('/nouveautés') 4 | }) 5 | 6 | it('has a title', () => { 7 | cy.get('[data-cypress-id="news-title"]').should('be.visible') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/integration/personas.cy.js: -------------------------------------------------------------------------------- 1 | describe('check for about page status', () => { 2 | beforeEach(() => { 3 | cy.visit('/personas') 4 | }) 5 | 6 | it('has a title', () => { 7 | cy.get('[data-cypress-id="personas-title"]').should('be.visible') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/pages/simulation.cy.js: -------------------------------------------------------------------------------- 1 | import { 2 | clickDoTheTestLink, 3 | clickSkipTutoButton, 4 | encodedRespirationParam, 5 | mainSimulator, 6 | waitWhileLoading, 7 | } from '../utils' 8 | 9 | describe('bug #1: going back to the simulation page after visiting the tutorial page', () => { 10 | it('should show the transport respiration', () => { 11 | cy.visit('') 12 | clickDoTheTestLink() 13 | waitWhileLoading() 14 | cy.get('[data-cypress-id="home-logo-link"]').click() 15 | clickDoTheTestLink() 16 | clickSkipTutoButton() 17 | cy.url().should( 18 | 'eq', 19 | Cypress.config().baseUrl + 20 | `/simulateur/${mainSimulator}?${encodedRespirationParam}=transport` 21 | ) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /cypress/e2e/test-completion/default-FR-fr.cy.js: -------------------------------------------------------------------------------- 1 | import { 2 | clickSeeResultsLink, 3 | defaultTotalValue, 4 | startTestAndSkipTutorial, 5 | walkthroughTest, 6 | } from '../utils' 7 | 8 | describe('check for test completion', () => { 9 | beforeEach(() => { 10 | cy.visit( 11 | `/?loc=${Cypress.env('localisation_param')}&lang=${Cypress.env( 12 | 'language_param' 13 | )}` 14 | ) 15 | }) 16 | 17 | it('can finish the test with the default values with loc=FR and lang=fr', () => { 18 | startTestAndSkipTutorial() 19 | walkthroughTest({}) 20 | clickSeeResultsLink() 21 | cy.contains(defaultTotalValue).should('be.visible') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /cypress/e2e/test-completion/default.cy.js: -------------------------------------------------------------------------------- 1 | import { 2 | clickSeeResultsLink, 3 | startTestAndSkipTutorial, 4 | walkthroughTest, 5 | } from '../utils' 6 | 7 | describe('check for test completion', () => { 8 | it('can finish the test with the default values with unspecified search params', () => { 9 | cy.visit('/') 10 | startTestAndSkipTutorial() 11 | walkthroughTest({}) 12 | clickSeeResultsLink() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /cypress/scripts/generateSpecsFromPersonas.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('fs') 2 | const { parse } = require('yaml') 3 | 4 | const personas = parse( 5 | readFileSync('./nosgestesclimat/personas/personas-fr.yaml', 'utf8') 6 | ) 7 | 8 | const getFileContent = (name, data) => ` 9 | import { walkthroughTest, startTestAndSkipTutorial, clickSeeResultsLink } from '../utils' 10 | describe('check for test completion', () => { 11 | it("can finish the test with persona '${name}' values", () => { 12 | cy.session('${name}', () => { 13 | cy.visit(\`/?loc=\${Cypress.env('localisation_param')}&lang=\${Cypress.env('language_param')}\`) 14 | startTestAndSkipTutorial() 15 | walkthroughTest(${JSON.stringify(data.data)}) 16 | clickSeeResultsLink() 17 | }) 18 | }) 19 | }) 20 | ` 21 | 22 | Object.entries(personas).map(([dottedName, data]) => { 23 | const name = dottedName.split(' . ')[1] 24 | writeFileSync( 25 | `./cypress/e2e/test-completion/persona-${name}.cy.js`, 26 | getFileContent(name, data) 27 | ) 28 | console.log(`[OK] persona-${name}.cy.js`) 29 | }) 30 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/cypress/support/e2e.ts -------------------------------------------------------------------------------- /dist/demo-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

iframe paramétré

10 | Ci-dessous, nosgestesclimat.fr intégré comme un iframe paramétré. 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /dist/demo-iframeSimulation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Exemple d'intégration du test avec région fixée par l'intégrateur.

9 | Ci-dessous, nosgestesclimat.fr intégré comme un iframe paramétré ne 10 | contenant que la partie simulation du test. Dans cet exemple, la Suisse a 11 | été définie comme étant la région par défaut du test. 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/Publicodes.md: -------------------------------------------------------------------------------- 1 |

Publicodes

2 | 3 | The model of NGC 4 | ([`nosgestesclimat`](https://github.com/datagir/nosgestesclimat/tree/master/data)) 5 | is build with [publicodes](https://publi.codes/), an interpreter on top of an 6 | extended [YAML](https://yaml.org/) syntax used to models complex computations. 7 | 8 | For example this is an extract from the file [`./data/transport/transport . 9 | avion.publicodes`](https://github.com/datagir/nosgestesclimat/blob/master/data/transport/transport%20.%20avion.publicodes): 10 | 11 | ```yaml 12 | transport . avion . court courrier: 13 | formule: heures de vol * vitesse moyenne * empreinte par km 14 | 15 | transport . avion . court courrier . vitesse moyenne: 16 | formule: 600 / 1.3 17 | unité: km/h 18 | note: | 19 | Nous utilisons la vitesse moyenne de vol pour un Paris Toulouse. 20 | ``` 21 | 22 | There is dedicated [_mécanismes_](https://publi.codes/docs/m%C3%A9canismes) 23 | such as `formule` or `note` supported by default by the publicode interpreter. 24 | 25 | ## Custom mechanisms 26 | 27 | However, during the implementation of the NGC's website new _mécanismes_ were 28 | added on top of native ones: 29 | 30 | - [`mosaic`](https://github.com/datagir/nosgestesclimat-site/wiki/mosaic): 31 | used to specify a set of related questions needed to be answer at the same 32 | time. 33 | -------------------------------------------------------------------------------- /docs/_Footer.md: -------------------------------------------------------------------------------- 1 |

To contact us: contact@nosgestesclimat.fr

-------------------------------------------------------------------------------- /loaders/locale-yaml-loader.js: -------------------------------------------------------------------------------- 1 | const yaml = require('yaml') 2 | 3 | module.exports = function (content) { 4 | const jsContent = yaml.parse(content) 5 | 6 | //Remove keys that make the bundle heavier but are only useful for translation purposes, not in the UI 7 | const newObject = Array.isArray(jsContent) 8 | ? jsContent.map((entry) => 9 | Object.fromEntries( 10 | Object.entries(entry).filter(([key, value]) => !key.endsWith('.lock')) 11 | ) 12 | ) 13 | : { 14 | entries: Object.fromEntries( 15 | Object.entries(jsContent.entries).filter( 16 | ([key, value]) => !key.endsWith('.lock') 17 | ) 18 | ), 19 | } 20 | 21 | return 'module.exports = ' + JSON.stringify(newObject) 22 | } 23 | -------------------------------------------------------------------------------- /manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nos gestes climat", 3 | "description": "Calcule ton empreinte sur le climat", 4 | "display": "standalone", 5 | "lang": "fr", 6 | "orientation": "portrait-primary", 7 | "theme_color": "#532fc5", 8 | "icons": [ 9 | { 10 | "src": "/images/logo.svg", 11 | "sizes": "144x144", 12 | "type": "image/svg+xml" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # Since 11/07/2023, we do not use "/contribuer" but "questions-frequentes" for de the FAQ. 2 | [[redirects]] 3 | from = "/contribuer" 4 | to = "/questions-frequentes" 5 | status = 301 6 | force = true 7 | 8 | [[redirects]] 9 | from = "/conference/*" 10 | to = "/conférence/:splat" 11 | 12 | # As it is an SPA, here, all urls has to be supported in react 13 | [[redirects]] 14 | from = "/*" 15 | to = "/index.html" 16 | status = 200 17 | 18 | [dev] 19 | framework = "#custom" 20 | command = "yarn start" # Command to start your dev server 21 | port = 8888 # The port that the netlify dev will be accessible on 22 | targetPort = 8080 23 | publish = "dist" # If you use a _redirect file, provide the path to your static content folder 24 | 25 | [[edge_functions]] 26 | path = "/geolocation" 27 | function = "geolocation" 28 | 29 | [functions] 30 | node_bundler = "esbuild" 31 | 32 | -------------------------------------------------------------------------------- /netlify/functions/create-issue.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | 3 | let { GITHUB_TOKEN } = process.env 4 | 5 | exports.handler = async (event, context) => { 6 | let baseUrl = 'https://api.github.com/repos/', 7 | { repo, title, body, labels } = event.queryStringParameters, 8 | url = baseUrl + repo + '/issues', 9 | headers = { 10 | Authorization: `token ${GITHUB_TOKEN}`, 11 | Accept: 'application/vnd.github.symmetra-preview+json', 12 | }, 13 | options = { 14 | method: 'POST', 15 | headers: headers, 16 | mode: 'cors', 17 | cache: 'default', 18 | body: JSON.stringify({ 19 | title, 20 | body, 21 | labels: (labels && labels.split(',')) || ['Contribution'], 22 | }), 23 | } 24 | 25 | return fetch(url, options) 26 | .then((response) => { 27 | return response.json() 28 | }) 29 | .then((json) => ({ 30 | statusCode: 200, 31 | headers: { 32 | 'Content-Type': 'application/json;charset=utf-8', 33 | 34 | 'Access-Control-Allow-Origin': '*', 35 | }, 36 | body: JSON.stringify({ url: json['html_url'] }), 37 | })) 38 | .catch((error) => ({ statusCode: 422, body: String(error) })) 39 | } 40 | -------------------------------------------------------------------------------- /netlify/functions/decrypt-data.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/require-await */ 2 | import CryptoJS from 'crypto-js' 3 | 4 | /** 5 | * Decrypts data with AES (use to decrypt data from encrypt-data.ts) 6 | * @param {string} data - Data to decrypt 7 | */ 8 | exports.handler = async (event) => { 9 | const data = String(event.body) 10 | 11 | const decryptedData = CryptoJS.AES.decrypt( 12 | data, 13 | process.env.ENCRYPTION_KEY 14 | ).toString(CryptoJS.enc.Utf8) 15 | 16 | return { 17 | statusCode: 200, 18 | headers: { 19 | 'Content-Type': 'application/json;charset=utf-8', 20 | }, 21 | body: JSON.stringify(decryptedData), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /netlify/functions/encrypt-data.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/require-await */ 2 | import CryptoJS from 'crypto-js' 3 | 4 | /** 5 | * Encrypts data with AES 6 | * @param {string} data - Data to encrypt 7 | */ 8 | exports.handler = async (event) => { 9 | const data = JSON.parse(event.body as string) 10 | 11 | const encryptedData = CryptoJS.AES.encrypt( 12 | data, 13 | process.env.ENCRYPTION_KEY 14 | ).toString() 15 | 16 | return { 17 | statusCode: 200, 18 | headers: { 19 | 'Content-Type': 'application/json;charset=utf-8', 20 | }, 21 | body: JSON.stringify(encryptedData), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /netlify/functions/geolocation.js: -------------------------------------------------------------------------------- 1 | const countries = require('./countries.json') 2 | 3 | exports.handler = async function (event) { 4 | const code = event.headers['x-country'] || 'FR' 5 | return { 6 | statusCode: 200, 7 | body: JSON.stringify({ 8 | country: countries.find((country) => country.code === code), 9 | }), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /netlify/functions/get-newsletter-subscribers-number.ts: -------------------------------------------------------------------------------- 1 | import SibApiV3Sdk from 'sib-api-v3-sdk' 2 | 3 | const NGC_LIST_ID = 22 4 | 5 | type Data = { 6 | totalSubscribers: number 7 | } 8 | 9 | exports.handler = async () => { 10 | const defaultClient = SibApiV3Sdk.ApiClient.instance 11 | 12 | // Configure API key authorization: api-key 13 | const apiKey = defaultClient.authentications['api-key'] 14 | apiKey.apiKey = process.env.BREVO_API_KEY 15 | 16 | const contactApiInstance = new SibApiV3Sdk.ContactsApi() 17 | 18 | try { 19 | const data: Data = await contactApiInstance.getList(NGC_LIST_ID) 20 | 21 | return { 22 | statusCode: 200, 23 | headers: { 24 | 'Content-Type': 'application/json;charset=utf-8', 25 | 26 | 'Access-Control-Allow-Origin': '*', 27 | }, 28 | body: JSON.stringify(data.totalSubscribers), 29 | } 30 | } catch (e) { 31 | console.log(e) 32 | return { 33 | statusCode: 404, 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer'), require('tailwindcss')], 3 | } 4 | -------------------------------------------------------------------------------- /scripts/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | # We overwrite the default ESLint configuration for NodeJS scripts 2 | env: 3 | node: true 4 | rules: 5 | no-console: 0 6 | -------------------------------------------------------------------------------- /scripts/i18n/check-ui.js: -------------------------------------------------------------------------------- 1 | const utils = require('@incubateur-ademe/nosgestesclimat-scripts/utils') 2 | const cli = require('@incubateur-ademe/nosgestesclimat-scripts/cli') 3 | 4 | const paths = require('./paths') 5 | 6 | const { srcLang, destLangs, markdown } = cli.getArgs( 7 | 'Check missing translations for UI texts.', 8 | { source: true, target: true, markdown: true }, 9 | ) 10 | 11 | cli.printChecksResultTableHeader(markdown) 12 | 13 | destLangs.forEach((destLang) => { 14 | const missingTranslations = utils.getUiMissingTranslations( 15 | paths.UI[srcLang].withLock, 16 | paths.UI[destLang].withLock, 17 | ) 18 | const nbMissingTranslations = missingTranslations.length 19 | 20 | cli.printChecksResult( 21 | nbMissingTranslations, 22 | missingTranslations, 23 | 'UI texts', 24 | destLang, 25 | markdown, 26 | ) 27 | }) 28 | -------------------------------------------------------------------------------- /scripts/i18n/paths.js: -------------------------------------------------------------------------------- 1 | /* 2 | Simple module containing all paths implicated to the translation. 3 | */ 4 | 5 | const path = require('path') 6 | 7 | const utils = require('@incubateur-ademe/nosgestesclimat-scripts/utils') 8 | 9 | const localesDir = path.resolve('source/locales') 10 | const rulesTranslation = path.resolve('source/locales/rules-en.yaml') 11 | const i18nextParserConfig = path.resolve('scripts/i18n/parser.config.js') 12 | const staticAnalysisFrRes = path.resolve( 13 | 'source/locales/static-analysis-fr.json' 14 | ) 15 | const UI = Object.fromEntries( 16 | utils.availableLanguages.map((lang) => [ 17 | lang, 18 | { 19 | withLock: path.resolve(`source/locales/ui/ui-${lang}.yaml`), 20 | withoutLock: path.resolve(`source/locales/ui/ui-${lang}-min.yaml`), 21 | }, 22 | ]) 23 | ) 24 | 25 | const FAQ = Object.fromEntries( 26 | utils.availableLanguages.map((lang) => [ 27 | lang, 28 | { 29 | withLock: path.resolve(`source/locales/faq/FAQ-${lang}.yaml`), 30 | withoutLock: path.resolve(`source/locales/faq/FAQ-${lang}-min.yaml`), 31 | }, 32 | ]) 33 | ) 34 | 35 | module.exports = { 36 | localesDir, 37 | rulesTranslation, 38 | i18nextParserConfig, 39 | staticAnalysisFrRes, 40 | UI, 41 | FAQ, 42 | } 43 | -------------------------------------------------------------------------------- /source/AnimatedLoader.tsx: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
63 |
64 |
65 |
66 |
67 |
68 | ) 69 | -------------------------------------------------------------------------------- /source/components/CurrencyInput/CurrencyInput.css: -------------------------------------------------------------------------------- 1 | .currencyInput__container { 2 | display: flex !important; 3 | align-items: center; 4 | justify-content: flex-end; 5 | } 6 | 7 | .currencyInput__input:focus { 8 | outline: none; 9 | } 10 | .currencyInput__input { 11 | height: inherit; 12 | max-height: inherit; 13 | border: none; 14 | text-align: inherit; 15 | font-family: inherit; 16 | padding: 0; 17 | font-weight: inherit; 18 | min-width: 0; 19 | outline: none; 20 | margin: 0; 21 | width: inherit; 22 | color: inherit; 23 | background-color: transparent; 24 | font-size: inherit; 25 | } 26 | 27 | .currencyInput__input::-ms-clear { 28 | display: none; 29 | } 30 | -------------------------------------------------------------------------------- /source/components/ErrorFallback.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import Logo from './Logo' 4 | 5 | type Props = { 6 | error: Error 7 | largeScreen: boolean 8 | } 9 | 10 | export const ErrorFallback = ({ largeScreen, error }: Props) => { 11 | const { t } = useTranslation() 12 | 13 | useEffect(() => { 14 | const chunkFailedMessage = /Loading chunk [\d]+ failed/ 15 | if (error?.message && chunkFailedMessage.test(error.message)) { 16 | window.location.reload() 17 | } 18 | }, [error]) 19 | 20 | return ( 21 |
26 | 27 |

{t("Une erreur s'est produite")}

28 |

33 | {t( 34 | 'Notre équipe a été notifiée, nous allons résoudre le problème au plus vite.' 35 | )} 36 |

37 | 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /source/components/Feedback/Feedback.css: -------------------------------------------------------------------------------- 1 | .feedback-page { 2 | display: flex; 3 | justify-content: center; 4 | flex-wrap: wrap; 5 | padding-top: 0.6rem; 6 | padding-bottom: 0.6rem; 7 | background: var(--lightestColor); 8 | border-radius: 0.9rem; 9 | padding: 0.6rem 1rem; 10 | margin: 1rem 0; 11 | } 12 | .feedback-page button.link-button { 13 | margin: 0 0.6rem; 14 | } 15 | 16 | @media (min-width: 1200px) { 17 | .feedback-page .feedbackButtons { 18 | display: inline; 19 | } 20 | } 21 | 22 | .zammad-form > .form-group:nth-of-type(2) { 23 | display: none; 24 | } 25 | .zammad-form > .btn[type='submit'] { 26 | display: inline-block; 27 | padding: 0.4rem 0.8rem; 28 | color: inherit; 29 | text-decoration: none; 30 | border: 1px solid; 31 | /* outline: none; */ 32 | line-height: initial; 33 | display: inline-block; 34 | border-radius: 0.3rem; 35 | transition: all 0.15s; 36 | text-align: center; 37 | text-transform: uppercase; 38 | 39 | font-weight: normal; 40 | cursor: pointer; 41 | } 42 | -------------------------------------------------------------------------------- /source/components/Feedback/LinkToForm.tsx: -------------------------------------------------------------------------------- 1 | import { Trans } from 'react-i18next' 2 | 3 | // Envie de donner un coup de pouce ? Répondez à notre sondage sur le simulateur. 4 | export default function LinkToForm() { 5 | const hostname = new URL( 6 | new URLSearchParams(document.location.search).get('integratorUrl') || 7 | document.referrer || 8 | document.location.origin 9 | ).hostname.replace(/^www\.|^m\./, '') 10 | return ( 11 |
14 | 15 | 16 | Envie de donner un coup de pouce ?{' '} 17 | 21 | Répondez à notre sondage sur le simulateur. 22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /source/components/Feedback/about.md: -------------------------------------------------------------------------------- 1 | These files are used by mon-entreprise to get feedback on the different pages 2 | or sections of the website. Could be useful to us. 3 | -------------------------------------------------------------------------------- /source/components/IframeResizer.tsx: -------------------------------------------------------------------------------- 1 | import { getIsIframe } from '@/utils' 2 | import { useEffect } from 'react' 3 | 4 | export function IframeResizer() { 5 | const isIframe = getIsIframe() 6 | useEffect(() => { 7 | // The code below communicate with the iframe.js script on a host site 8 | // to automatically resize the iframe when its inner content height 9 | // change. 10 | 11 | if (!isIframe) { 12 | return 13 | } 14 | 15 | const minHeight = 800 // Also used in iframe.js 16 | const observer = new ResizeObserver(([entry]) => { 17 | const value = Math.max(minHeight, entry.contentRect.height) 18 | window.parent?.postMessage({ kind: 'resize-height', value }, '*') 19 | }) 20 | observer.observe(window.document.body) 21 | return () => observer.disconnect() 22 | }, [isIframe]) 23 | 24 | return null 25 | } 26 | -------------------------------------------------------------------------------- /source/components/IllustratedButton.tsx: -------------------------------------------------------------------------------- 1 | import emoji from 'react-easy-emoji' 2 | import { Link } from 'react-router-dom' 3 | 4 | export default ({ children, icon, to, onClick }) => ( 5 | 22 |
div { 29 | margin-left: 1.6rem; 30 | text-align: left; 31 | small { 32 | color: var(--textColor); 33 | } 34 | } 35 | h1, 36 | h2, 37 | h3, 38 | h4, 39 | h5 { 40 | color: white; 41 | } 42 | `} 43 | > 44 | {emoji(icon)} 45 | 46 | {children} 47 |
48 | 49 | ) 50 | -------------------------------------------------------------------------------- /source/components/Notifications.css: -------------------------------------------------------------------------------- 1 | blockingNotification { 2 | text-align: center; 3 | font-size: 150%; 4 | } 5 | 6 | #notificationsBlock > ul { 7 | list-style: none; 8 | padding: 0; 9 | } 10 | #notificationsBlock .notification { 11 | margin: 1em 0; 12 | display: flex; 13 | align-items: center; 14 | } 15 | #notificationsBlock .notificationText { 16 | width: 100%; 17 | padding: 1rem 2.6rem 1rem 1.6rem; 18 | /*For the .hide element */ 19 | position: relative; 20 | } 21 | #notificationsBlock .notificationText p:last-child { 22 | margin: 0; 23 | } 24 | 25 | .notificationText .hide { 26 | position: absolute; 27 | top: 0rem; 28 | right: -1.4rem; 29 | font-size: 200%; 30 | } 31 | 32 | #notificationExplanation { 33 | } 34 | #notificationExplanation > div { 35 | display: inline; 36 | } 37 | 38 | /*Disable links visually */ 39 | #notificationExplanation a { 40 | color: inherit; 41 | text-decoration: none; 42 | } 43 | 44 | /* Display the values of the variables in the explanation of the failed control */ 45 | #notificationsBlock .variable .situationValue { 46 | display: inline-block; 47 | } 48 | #notificationsBlock img { 49 | margin: 0 1em 0 !important; 50 | width: 1.6em !important; 51 | height: 1.6em !important; 52 | } 53 | -------------------------------------------------------------------------------- /source/components/PartnerBanner.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next' 2 | 3 | export default () => { 4 | const { t } = useTranslation() 5 | return ( 6 |
23 | 30 | Rejoignez le mouvement 31 | {t( 40 | 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /source/components/PercentageField.tsx: -------------------------------------------------------------------------------- 1 | import { formatValue } from 'publicodes' 2 | import { useCallback, useState } from 'react' 3 | import { useTranslation } from 'react-i18next' 4 | import { debounce as debounceFn } from '../utils' 5 | import { InputCommonProps } from './conversation/RuleInput' 6 | import './PercentageField.css' 7 | 8 | type PercentageFieldProps = InputCommonProps & { debounce: number } 9 | 10 | export default function PercentageField({ 11 | onChange, 12 | nodeValue: value, 13 | debounce = 0, 14 | }: PercentageFieldProps) { 15 | const [localValue, setLocalValue] = useState(value) 16 | const debouncedOnChange = useCallback( 17 | debounce ? debounceFn(debounce, onChange) : onChange, 18 | [debounce, onChange] 19 | ) 20 | const language = useTranslation().i18n.language 21 | 22 | return ( 23 |
24 | { 27 | const value = e.target.value 28 | setLocalValue(value) 29 | debouncedOnChange(value) 30 | }} 31 | type="range" 32 | value={localValue} 33 | name="volume" 34 | min="0" 35 | step="0.05" 36 | max="1" 37 | /> 38 | 39 | {formatValue(localValue, { 40 | language, 41 | displayedUnit: '%', 42 | })} 43 | 44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /source/components/PeriodSwitch.css: -------------------------------------------------------------------------------- 1 | #PeriodSwitch { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | } 6 | -------------------------------------------------------------------------------- /source/components/PeriodSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { updateUnit } from 'Actions/actions' 2 | import { Trans } from 'react-i18next' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { targetUnitSelector } from 'Selectors/simulationSelectors' 5 | import './PeriodSwitch.css' 6 | 7 | export default function PeriodSwitch() { 8 | const dispatch = useDispatch() 9 | const currentUnit = useSelector(targetUnitSelector) 10 | 11 | const units = ['€/mois', '€/an'] 12 | return ( 13 | 14 | 15 | {units.map((unit) => ( 16 | 28 | ))} 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /source/components/Route404.tsx: -------------------------------------------------------------------------------- 1 | import image from 'Images/map-directions.png' 2 | import emoji from 'react-easy-emoji' 3 | import { Trans } from 'react-i18next' 4 | import { Link } from 'react-router-dom' 5 | 6 | export default () => ( 7 |
15 |

16 | 17 | Cette page n'existe pas ou n'existe plus 18 | 19 | {emoji(' 🙅')} 20 |

21 | 22 | {/* TODO: credits for the image to add: https://thenounproject.com/term/treasure-map/96666/ */} 23 | 24 | 25 | Revenir en lieu sûr 26 | 27 | 28 |
29 | ) 30 | -------------------------------------------------------------------------------- /source/components/RuleLink.tsx: -------------------------------------------------------------------------------- 1 | import { RuleLink as EngineRuleLink } from 'publicodes-react' 2 | import React, { useContext } from 'react' 3 | import { Link } from 'react-router-dom' 4 | import { EngineContext } from './utils/EngineContext' 5 | import { SitePathsContext } from './utils/SitePathsContext' 6 | 7 | export default function RuleLink( 8 | props: { 9 | dottedName: Object 10 | displayIcon?: boolean 11 | } & Omit, 'to'> 12 | ) { 13 | const sitePaths = useContext(SitePathsContext) 14 | const engine = useContext(EngineContext) 15 | return ( 16 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /source/components/SafeCategoryImage.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export default ({ element, whiteBackground = false, voidIfFail }) => { 4 | if (element.dottedName === 'rest') return 5 | const [fail, setFail] = useState(false) 6 | return ( 7 | 26 | {!fail ? ( 27 | { 30 | currentTarget.onerror = null 31 | setFail(true) 32 | }} 33 | /> 34 | ) : ( 35 | !voidIfFail && 36 | )} 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /source/components/SearchBar.css: -------------------------------------------------------------------------------- 1 | li.active { 2 | background: var(--color); 3 | color: var(--textColor); 4 | } 5 | 6 | li.active a { 7 | color: var(--textColor); 8 | } 9 | -------------------------------------------------------------------------------- /source/components/SearchBar.worker.js: -------------------------------------------------------------------------------- 1 | import Fuse from 'fuse.js' 2 | 3 | let searchWeights = [ 4 | { 5 | name: 'espace', 6 | weight: 0.6 7 | }, 8 | { 9 | name: 'title', 10 | weight: 0.4 11 | } 12 | ] 13 | 14 | let fuse = null 15 | onmessage = function(event) { 16 | if (event.data.rules) 17 | fuse = new Fuse(event.data.rules, { 18 | keys: searchWeights, 19 | includeMatches: true, 20 | minMatchCharLength: 2, 21 | useExtendedSearch: true, 22 | distance: 50, 23 | threshold: 0.3 24 | }) 25 | 26 | if (event.data.input) { 27 | let results = [ 28 | ...fuse.search( 29 | event.data.input + '|' + event.data.input.replace(/ /g, '|') 30 | ) 31 | ] 32 | postMessage(results) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/components/SearchButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Trans } from 'react-i18next' 3 | import { Navigate } from 'react-router' 4 | import useKeypress from '../hooks/useKeyPress' 5 | 6 | type SearchButtonProps = { 7 | invisibleButton?: boolean 8 | } 9 | 10 | export default function SearchButton({ invisibleButton }: SearchButtonProps) { 11 | const [visible, setVisible] = useState(false) 12 | 13 | useKeypress( 14 | 'k', 15 | true, 16 | (e) => { 17 | e.preventDefault() 18 | setVisible(true) 19 | }, 20 | 'keydown', 21 | [] 22 | ) 23 | 24 | const close = () => setVisible(false) 25 | 26 | return visible ? ( 27 | 28 | ) : ( 29 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /source/components/Simulation.tsx: -------------------------------------------------------------------------------- 1 | import Conversation, { 2 | ConversationProps, 3 | } from '@/components/conversation/Conversation' 4 | import * as animate from '@/components/ui/animate' 5 | 6 | import React from 'react' 7 | 8 | type SimulationProps = { 9 | explanations?: React.ReactNode 10 | results?: React.ReactNode 11 | showPeriodSwitch?: boolean 12 | showLinkToForm?: boolean 13 | animation?: keyof typeof animate 14 | conversationProps: Partial 15 | } 16 | 17 | export default function Simulation({ 18 | explanations, 19 | results, 20 | animation = 'appear', 21 | conversationProps, 22 | }: SimulationProps) { 23 | const Animation = animate[animation] 24 | return ( 25 | <> 26 | 27 | {results} 28 | 29 | {explanations} 30 | 31 | 32 | ) 33 | } 34 | 35 | function Questions({ 36 | conversationProps, 37 | }: { 38 | conversationProps: Partial 39 | }) { 40 | return ( 41 | <> 42 |
51 | 52 |
53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /source/components/conversation/Aide.css: -------------------------------------------------------------------------------- 1 | #helpWrapper { 2 | margin: 2em auto 0; 3 | display: none; 4 | } 5 | 6 | #help { 7 | border-radius: 0.4em; 8 | padding: 0.1em 1em; 9 | background: #e7f8e1; 10 | border: 1px solid #9fbd94; 11 | position: relative; 12 | } 13 | 14 | #help blockquote { 15 | font-style: italic; 16 | font-size: 95%; 17 | opacity: 0.95; 18 | border-left: 4px solid #4b4b66; 19 | margin-left: 0.3em; 20 | padding-left: 0.6em; 21 | } 22 | 23 | #helpWrapper.active { 24 | display: block; 25 | } 26 | 27 | #help #closeHelp { 28 | font-size: 150%; 29 | position: absolute; 30 | top: 0.4em; 31 | right: 0em; 32 | cursor: pointer; 33 | } 34 | 35 | #help h3 { 36 | margin: 0.3em 0; 37 | } 38 | -------------------------------------------------------------------------------- /source/components/conversation/Aide.tsx: -------------------------------------------------------------------------------- 1 | import { explainVariable } from '@/actions/actions' 2 | import animate from '@/components/ui/animate' 3 | import { Markdown } from '@/components/utils/markdown' 4 | import { AppState } from '@/reducers/rootReducer' 5 | import { Trans } from 'react-i18next' 6 | import { useDispatch, useSelector } from 'react-redux' 7 | import './Aide.css' 8 | 9 | export default function Aide() { 10 | const explained = useSelector((state: AppState) => state.explainedVariable) 11 | const rules = useSelector((state: AppState) => state.rules) 12 | 13 | const dispatch = useDispatch() 14 | 15 | const stopExplaining = () => dispatch(explainVariable()) 16 | 17 | if (!explained) { 18 | return null 19 | } 20 | 21 | const rule = rules[explained] 22 | const text = rule.description 23 | 24 | if (text === undefined) { 25 | return null 26 | } 27 | 28 | return ( 29 | 30 |
button { 35 | text-align: right; 36 | } 37 | `} 38 | > 39 | {text} 40 | 43 |
44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /source/components/conversation/AnswerList.css: -------------------------------------------------------------------------------- 1 | .answer-list table { 2 | border-collapse: collapse; 3 | width: 100%; 4 | } 5 | .answer-list tr:nth-child(2n + 1) { 6 | background: none !important; 7 | } 8 | 9 | .answer-list td { 10 | padding: 0.3rem 0.8rem; 11 | } 12 | 13 | .answer-list td:last-of-type { 14 | text-align: start; 15 | } 16 | 17 | .answer-list button.answer { 18 | } 19 | .answer-list .answerContent { 20 | } 21 | 22 | .answer-list button.answer:hover { 23 | opacity: 0.8; 24 | } 25 | -------------------------------------------------------------------------------- /source/components/conversation/Explicable.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | .explicable { 3 | display: none; 4 | } 5 | } 6 | .explicable .icon { 7 | display: inline-block; 8 | padding: 0.15rem 0.6rem; 9 | height: 100%; 10 | margin-left: 0.2rem; 11 | text-align: center; 12 | cursor: pointer; 13 | vertical-align: text-top; 14 | } 15 | 16 | .explicable .icon:hover { 17 | filter: brightness(90%); 18 | } 19 | 20 | .variantLeaf .explicable .icon img { 21 | width: 1.5em !important; 22 | height: 1.5em !important; 23 | } 24 | -------------------------------------------------------------------------------- /source/components/conversation/ParagrapheInput.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { debounce } from '../../utils' 3 | import { InputCommonProps } from './RuleInput' 4 | 5 | export default function ParagrapheInput({ 6 | onChange, 7 | nodeValue: value, 8 | id, 9 | defaultValue, 10 | autoFocus, 11 | }: InputCommonProps) { 12 | const debouncedOnChange = useCallback(debounce(1000, onChange), []) 13 | 14 | return ( 15 |
16 |