├── .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 |
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 |
{
40 | window.location.reload()
41 | }}
42 | >
43 | {t('Recharger la page')}
44 |
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 |
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 |
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 |
17 | dispatch(updateUnit(unit))}
22 | checked={currentUnit === unit}
23 | />
24 |
25 | {unit}
26 |
27 |
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 | setVisible(true)}
33 | >
34 | 🔍 Rechercher
35 |
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 |
41 | Refermer
42 |
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 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/source/components/conversation/QuestionFinder.worker.ts:
--------------------------------------------------------------------------------
1 | import Fuse from 'fuse.js'
2 |
3 | const searchWeights = [
4 | {
5 | name: 'espace',
6 | weight: 0.2,
7 | },
8 | {
9 | name: 'title',
10 | weight: 0.2,
11 | },
12 | {
13 | name: 'question',
14 | weight: 0.4,
15 | },
16 | {
17 | name: 'question',
18 | weight: 0.2,
19 | },
20 | ]
21 |
22 | let fuse = null
23 | onmessage = function (event) {
24 | if (event.data.rules)
25 | fuse = new Fuse(event.data.rules, {
26 | keys: searchWeights,
27 | includeMatches: true,
28 | minMatchCharLength: 2,
29 | useExtendedSearch: true,
30 | distance: 50,
31 | threshold: 0.3,
32 | })
33 |
34 | if (event.data.input) {
35 | const results = [
36 | ...fuse.search(
37 | event.data.input + '|' + event.data.input.replace(/ /g, '|')
38 | ),
39 | ]
40 | postMessage(results)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/source/components/conversation/SeeAnswersButton.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { Trans } from 'react-i18next'
3 | import Answers from './AnswerList'
4 | import './conversation.css'
5 |
6 | export default function SeeAnswersButton() {
7 | const [showAnswerModal, setShowAnswerModal] = useState(false)
8 | return (
9 | <>
10 | setShowAnswerModal(true)}
13 | >
14 | Voir ma situation
15 |
16 | {showAnswerModal && setShowAnswerModal(false)} />}
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/source/components/conversation/SimulationEnding.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 |
3 | export default ({ customEnd, customEndMessages }) => {
4 | return (
5 |
6 | {customEnd ?? (
7 | <>
8 |
9 |
10 | 🌟 Vous avez complété cette simulation
11 |
12 |
13 |
14 | {customEndMessages ? (
15 | customEndMessages
16 | ) : (
17 |
18 | Vous avez maintenant accès à l'estimation la plus précise
19 | possible.
20 |
21 | )}
22 |
23 | >
24 | )}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/source/components/conversation/TextInput.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { debounce } from '../../utils'
3 | import { InputCommonProps } from './RuleInput'
4 |
5 | export default function TextInput({
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 | {
23 | debouncedOnChange(`'${target.value}'`)
24 | }}
25 | defaultValue={value}
26 | autoComplete="off"
27 | />
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/source/components/conversation/UI.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 |
3 | export const CategoryLabel = styled.span`
4 | background: 'darkblue';
5 | color: var(--darkColor);
6 | border-radius: 0.3rem;
7 | text-transform: uppercase;
8 | margin-right: 0.6rem;
9 | display: flex;
10 | align-items: center;
11 | img {
12 | font-size: 140%;
13 | margin: 0 0.6rem 0 0 !important;
14 | }
15 |
16 | font-size: 140%;
17 | font-weight: 600;
18 | opacity: 0.6;
19 | img {
20 | font-size: 100%;
21 | display: none;
22 | }
23 |
24 | ${(props) =>
25 | props.color &&
26 | css`
27 | background: ${props.color};
28 | `}
29 | `
30 |
--------------------------------------------------------------------------------
/source/components/conversation/amortissement-avion/AmortissementButton.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import emoji from '../../emoji'
3 |
4 | export default function AmortissementButton({ text, onHandleClick }) {
5 | const [hover, setHover] = useState(false)
6 | return (
7 | setHover(true)}
12 | onMouseLeave={() => setHover(false)}
13 | >
14 |
22 | {emoji('✋', '')}
23 | {text}
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/source/components/conversation/estimate/KmEstimation.tsx:
--------------------------------------------------------------------------------
1 | import { Evaluation } from 'publicodes'
2 | import { InputHTMLAttributes, useState } from 'react'
3 | import { DottedName } from 'Rules'
4 | import KmHelp from './KmHelp'
5 | import KmInput from './KmHelp/KmInput'
6 |
7 | interface Props {
8 | commonProps: InputHTMLAttributes & {
9 | dottedName: DottedName
10 | }
11 | evaluation: Evaluation
12 | onSubmit: (value: string) => void
13 | setFinalValue: (value: string) => void
14 | value: string
15 | }
16 |
17 | export default function KmEstimation({
18 | commonProps,
19 | evaluation,
20 | onSubmit,
21 | setFinalValue,
22 | value,
23 | }: Props) {
24 | const [isFormOpen, setIsFormOpen] = useState(false)
25 | return (
26 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/source/components/conversation/estimate/KmHelp/KmHelpButton.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import styled from 'styled-components'
3 |
4 | export default function KmHelpButton({ text, onHandleClick }) {
5 | const [hover, setHover] = useState(false)
6 | return (
7 | setHover(true)}
11 | onMouseLeave={() => setHover(false)}
12 | >
13 |
21 | {text}
22 |
23 |
24 | )
25 | }
26 |
27 | const StyledButton = styled.button`
28 | font-size: 0.875rem;
29 | background-color: rgb(253 230 138);
30 | padding: 0.5rem;
31 | border-radius: 0.25rem;
32 | margin-bottom: 0.5rem;
33 | line-height: 1;
34 | transition: background-color 0.2s;
35 | background-size: 280%;
36 | &:hover {
37 | background-color: #fcdb54;
38 | background-position-x: 0%;
39 | border-color: white !important;
40 | background-image: linear-gradient(
41 | 50deg,
42 | var(--darkestColor) -50%,
43 | #fcdb54 10%
44 | );
45 | }
46 | `
47 |
--------------------------------------------------------------------------------
/source/components/conversation/estimate/KmHelp/KmInput.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import styled from 'styled-components'
3 | import Input from '../../Input'
4 |
5 | const helperId = 'km-helper-id'
6 |
7 | const KmInput = (props) => {
8 | const { t } = useTranslation()
9 | const { isFormOpen } = props
10 | return (
11 | <>
12 |
19 | {isFormOpen && (
20 |
21 | {t(
22 | 'Champ désactivé durant le remplissage du détail ; se mettra à jour automatiquement.'
23 | )}
24 |
25 | )}
26 | >
27 | )
28 | }
29 |
30 | const StyledSpan = styled.span`
31 | text-align: right;
32 | font-size: 0.75rem;
33 | display: block;
34 | margin-top: -0.25rem;
35 | margin-bottom: 0.5rem;
36 | line-height: 1.5;
37 | `
38 |
39 | export default KmInput
40 |
--------------------------------------------------------------------------------
/source/components/conversation/estimate/KmHelp/ReadOnlyRow.js:
--------------------------------------------------------------------------------
1 | export default function ReadOnlyRow({
2 | trajet,
3 | setEditFormData,
4 | setEditTrajetId,
5 | trajets,
6 | setTrajets,
7 | openmojiURL,
8 | }) {
9 | const handleEditClick = (event, trajet) => {
10 | event.preventDefault()
11 | setEditTrajetId(trajet.id)
12 |
13 | const formValues = { ...trajet }
14 |
15 | setEditFormData(formValues)
16 | }
17 |
18 | const handleDeleteClick = (trajetId) => {
19 | const newTrajets = [...trajets]
20 |
21 | const index = trajets.findIndex((trajet) => trajet.id === trajetId)
22 |
23 | newTrajets.splice(index, 1)
24 |
25 | setTrajets(newTrajets)
26 | }
27 |
28 | return (
29 |
30 | {trajet.motif}
31 | {trajet.label}
32 | {trajet.distance}
33 |
34 | {trajet.xfois} x / {trajet.periode}
35 |
36 | {trajet.personnes}
37 | button {
40 | padding: 0.4rem;
41 | }
42 | `}
43 | >
44 | handleEditClick(event, trajet)}
47 | >
48 |
49 |
50 | handleDeleteClick(trajet.id)}>
51 |
52 |
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/source/components/conversation/estimate/KmHelp/dataHelp.js:
--------------------------------------------------------------------------------
1 | export const motifList = (t) => [
2 | {
3 | name: t('Vacances'),
4 | id: '1v',
5 | },
6 | {
7 | name: t('Domicile-Travail'),
8 | id: '2dt',
9 | },
10 | {
11 | name: t('Visite familiale'),
12 | id: '3vf',
13 | },
14 | {
15 | name: t('Mobilité académique'),
16 | id: '4ma',
17 | },
18 | {
19 | name: t('Sport ou Loisir'),
20 | id: '5sl',
21 | },
22 | {
23 | name: t('Sorties ponctuelles'),
24 | id: '6sp',
25 | },
26 | {
27 | name: t('Courses'),
28 | id: '7c',
29 | },
30 | {
31 | name: t('RDV médicaux'),
32 | id: '8rm',
33 | },
34 | {
35 | name: t('Week-end'),
36 | id: '9we',
37 | },
38 | ]
39 |
40 | export const freqList = (t) => [
41 | {
42 | name: t('jour'),
43 | id: '1j',
44 | value: 365,
45 | },
46 | {
47 | name: t('semaine'),
48 | id: '1s',
49 | value: 52,
50 | },
51 | {
52 | name: t('mois'),
53 | id: '1m',
54 | value: 12,
55 | },
56 | {
57 | name: t('an'),
58 | id: '1a',
59 | value: 1,
60 | },
61 | ]
62 |
--------------------------------------------------------------------------------
/source/components/conversation/select/MosaicStamp.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const MosaicStamp = styled.div`
4 | font-size: 75%;
5 | font-weight: bold;
6 | display: inline-block;
7 | padding: 0.2rem 0.2rem 0;
8 | text-transform: uppercase;
9 | text-align: center;
10 | font-family: 'Courier';
11 | mix-blend-mode: multiply;
12 | mask-position: 13rem 6rem;
13 | transform: rotate(-10deg);
14 | border-radius: 4px;
15 | line-height: 1rem;
16 | z-index: 0;
17 | max-width: 8rem;
18 | border: 3px solid var(--darkColor);
19 | color: var(--darkColor);
20 | cursor: not-allowed !important;
21 | `
22 |
23 | export default MosaicStamp
24 |
--------------------------------------------------------------------------------
/source/components/groupe/ButtonLink.tsx:
--------------------------------------------------------------------------------
1 | import { colorClassNames, sizeClassNames } from '@/components/groupe/Button'
2 | import { ButtonSize } from '@/types/values'
3 | import { Link } from 'react-router-dom'
4 |
5 | type Props = {
6 | href: string
7 | className?: string
8 | color?: 'primary' | 'secondary'
9 | size?: ButtonSize
10 | } & React.PropsWithChildren
11 |
12 | // Create a button component styled with tailwindcss
13 | export default function ButtonLink({
14 | href,
15 | children,
16 | className = '',
17 | color = 'primary',
18 | size = 'md',
19 | ...props
20 | }: Props) {
21 | return (
22 |
27 | {children}
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/source/components/groupe/Container.tsx:
--------------------------------------------------------------------------------
1 | export default function Container({
2 | children,
3 | className,
4 | }: { className: string } & React.PropsWithChildren) {
5 | return {children}
6 | }
7 |
--------------------------------------------------------------------------------
/source/components/groupe/CopyInput.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@/components/groupe/Button'
2 | import { useState } from 'react'
3 | import { Trans } from 'react-i18next'
4 |
5 | type Props = {
6 | textToCopy: string
7 | className?: string
8 | }
9 |
10 | export default function CopyInput({ textToCopy, className = '' }: Props) {
11 | const [isCopied, setIsCopied] = useState(false)
12 |
13 | return (
14 |
15 |
21 | {
25 | navigator.clipboard.writeText(textToCopy)
26 | setIsCopied(true)
27 | setTimeout(() => setIsCopied(false), 3000)
28 | }}
29 | >
30 | {isCopied ? Copié ! : Copier le lien }
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/source/components/groupe/EmailInput.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import TextInputGroup from './TextInputGroup'
3 |
4 | export default function EmailInput({
5 | email,
6 | setEmail,
7 | errorEmail,
8 | setErrorEmail,
9 | ...props
10 | }) {
11 | const { t } = useTranslation()
12 |
13 | return (
14 |
17 | {t('Votre adresse email')}{' '}
18 | {t('facultatif')}
19 |
20 | }
21 | helperText={t(
22 | 'Seulement pour vous permettre de retrouver votre groupe ou de supprimer vos données'
23 | )}
24 | name="prenom"
25 | placeholder="jean-marc@nosgestesclimat.fr"
26 | className="mt-6 mb-6"
27 | onChange={(e: React.ChangeEvent) => {
28 | setEmail(e.target.value)
29 | if (errorEmail) {
30 | setErrorEmail('')
31 | }
32 | }}
33 | value={email}
34 | error={errorEmail}
35 | {...props}
36 | />
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/source/components/groupe/GoBackLink.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import Link from './Link'
3 |
4 | export default function GoBackLink({ className }: { className?: string }) {
5 | return (
6 |
10 | ← Retour
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/source/components/groupe/Link.tsx:
--------------------------------------------------------------------------------
1 | import { Link as RouterLink } from 'react-router-dom'
2 |
3 | type Props = {
4 | href: string
5 | className?: string
6 | } & React.PropsWithChildren
7 |
8 | export default function Link({ children, href, className }: Props) {
9 | return (
10 |
14 | {children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/source/components/groupe/PrenomInput.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import TextInputGroup from './TextInputGroup'
3 |
4 | export default function PrenomInput({
5 | prenom,
6 | setPrenom,
7 | errorPrenom,
8 | setErrorPrenom,
9 | ...props
10 | }) {
11 | const { t } = useTranslation()
12 |
13 | return (
14 | ) => {
23 | setPrenom(e.target.value)
24 | if (errorPrenom) {
25 | setErrorPrenom('')
26 | }
27 | }}
28 | error={errorPrenom}
29 | value={prenom}
30 | {...props}
31 | />
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/source/components/groupe/Separator.tsx:
--------------------------------------------------------------------------------
1 | export default function Separator({ className = '' }) {
2 | return
3 | }
4 |
--------------------------------------------------------------------------------
/source/components/groupe/Title.tsx:
--------------------------------------------------------------------------------
1 | import Separator from './Separator'
2 |
3 | type Props = {
4 | title: string | JSX.Element
5 | subtitle?: string
6 | tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
7 | }
8 |
9 | export default function Title({
10 | title,
11 | subtitle,
12 | tag = 'h1',
13 | ...props
14 | }: Props) {
15 | const Tag = tag
16 | return (
17 |
18 |
19 | {title}
20 |
21 |
22 | {subtitle &&
{subtitle}
}
23 |
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/source/components/highlightMatches.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function highlightMatches(str: string, matches: Matches) {
4 | if (!matches?.length) {
5 | return str
6 | }
7 | const indices = matches[0].indices
8 | .sort(([a], [b]) => a - b)
9 | .map(([x, y]) => [x, y + 1])
10 | .reduce(
11 | (acc, value) =>
12 | acc[acc.length - 1][1] <= value[0] ? [...acc, value] : acc,
13 | [[0, 0]]
14 | )
15 | .flat()
16 | return [...indices, str.length].reduce(
17 | ([highlight, prevIndice, acc], currentIndice, i) => {
18 | const currentStr = str.slice(prevIndice, currentIndice)
19 | return [
20 | !highlight,
21 | currentIndice,
22 | [
23 | ...acc,
24 |
29 | {currentStr}
30 | ,
31 | ],
32 | ] as [boolean, number, Array]
33 | },
34 | [false, 0, []] as [boolean, number, Array]
35 | )[2]
36 | }
37 |
--------------------------------------------------------------------------------
/source/components/icons/ChevronRight.tsx:
--------------------------------------------------------------------------------
1 | export default function ChevronRight({ className = '' }) {
2 | return (
3 |
10 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/source/components/localisation/CountryFlag.tsx:
--------------------------------------------------------------------------------
1 | import { RegionCode, useFlag } from './utils'
2 |
3 | export default ({ code }: { code?: RegionCode }) => {
4 | const flagSrc = useFlag(code)
5 |
6 | return (
7 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/source/components/localisation/RegionModelAuthors.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 |
3 | export type RegionAuthor = {
4 | nom: string
5 | url?: string
6 | }
7 |
8 | export const DEFAULT_REGION_MODEL_AUTHOR: RegionAuthor = {
9 | nom: 'l’équipe de nosgestesclimat.fr',
10 | url: 'https://nosgestesclimat.fr/à-propos',
11 | }
12 |
13 | export default ({ authors = [] }: { authors?: RegionAuthor[] }) => {
14 | const { t } = useTranslation()
15 |
16 | return (
17 |
18 | {authors.length > 0 && (
19 |
20 | {t('Ce modèle a été conçu par')}{' '}
21 | {authors.map((author, i) => (
22 |
23 |
24 | {author.nom}
25 |
26 | {i !== authors.length - 1 && ' ' + t('et') + ' '}
27 |
28 | ))}
29 |
30 | )}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/source/components/localisation/useCurrentRegionCode.ts:
--------------------------------------------------------------------------------
1 | export default function useCurrentRegionCode() {
2 | return supportedRegion(localisation?.country?.code)
3 | ? localisation?.country?.code
4 | : defaultModel
5 | }
6 |
--------------------------------------------------------------------------------
/source/components/localisation/useOrderedSupportedRegions.ts:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux'
2 | import { AppState } from '../../reducers/rootReducer'
3 |
4 | export default () => {
5 | const currentLang = useSelector(
6 | (state: AppState) => state.currentLang
7 | ).toLowerCase()
8 |
9 | const supportedRegions = useSelector(
10 | (state: AppState) => state.supportedRegions
11 | )
12 | // Regions displayed sorted alphabetically
13 | const orderedSupportedRegions = Object.fromEntries(
14 | Object.entries(supportedRegions)
15 | // sort function from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
16 | .sort((a, b) => {
17 | const nameA = a[1][currentLang]?.nom.toUpperCase() // ignore upper and lowercase
18 | const nameB = b[1][currentLang]?.nom.toUpperCase() // ignore upper and lowercase
19 | if (nameA < nameB) {
20 | return -1
21 | }
22 | if (nameA > nameB) {
23 | return 1
24 | }
25 | // names must be equal
26 | return 0
27 | })
28 | )
29 | return orderedSupportedRegions
30 | }
31 |
--------------------------------------------------------------------------------
/source/components/stats/content/chart/Search.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | import FancySelect from '../../utils/FancySelect'
4 |
5 | import { useTranslation } from 'react-i18next'
6 | import { range } from '../../../../utils'
7 |
8 | const Wrapper = styled.div`
9 | margin-bottom: 0.5rem;
10 | text-align: right;
11 | font-size: 0.85rem;
12 |
13 | @media screen and (max-width: ${800}px) {
14 | font-size: 0.75rem;
15 | }
16 | `
17 | export default function Search(props) {
18 | const { t } = useTranslation()
19 |
20 | return (
21 |
22 | {t('Nombre de')} {props.elementAnalysedTitle} {t('pour les ')}
23 | {
27 | props.setDate(e)
28 | }}
29 | options={range(4, 31).map((elt) => ({
30 | value: String(elt),
31 | label: String(elt),
32 | }))}
33 | />{' '}
34 | {props.period === 'week' ? t('dernières') : t('derniers')}{' '}
35 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/source/components/stats/utils/Section.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Section = styled.section`
4 | margin: 0 auto 1rem;
5 | font-family: 'Marianne', sans-serif;
6 | padding: 0;
7 | `
8 | Section.TopTitle = styled.h1`
9 | font-size: 2rem;
10 | margin-bottom: 1.5rem !important;
11 | font-family: 'Marianne', sans-serif;
12 |
13 | @media screen and (max-width: ${800}px) {
14 | font-size: 2rem;
15 | }
16 | `
17 | Section.Title = styled.h2`
18 | font-size: 1.5em;
19 | font-family: 'Marianne', sans-serif;
20 | margin-bottom: 1rem;
21 | `
22 |
23 | Section.Intro = styled.details`
24 | margin: 1rem 0 1rem 0;
25 | p {
26 | font-size: 1rem;
27 | line-height: 1.3rem;
28 | }
29 | > summary {
30 | font-size: 1.3rem;
31 | margin-bottom: 0.5rem;
32 | }
33 | `
34 |
35 | Section.Sector = styled.span`
36 | color: red;
37 | `
38 |
39 | export default Section
40 |
--------------------------------------------------------------------------------
/source/components/ui/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './button.css'
3 |
4 | export const LinkButton = (props: React.HTMLAttributes) => (
5 |
6 | )
7 |
8 | export const Button = (props: React.HTMLAttributes) => (
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/source/components/ui/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './index.css'
3 |
4 | export default function Checkbox(
5 | props: React.ComponentProps<'input'> & { label?: string; showLabel?: boolean }
6 | ) {
7 | return (
8 | <>
9 |
25 |
29 |
35 | {props.label && (
36 |
43 | {props.label}
44 |
45 | )}
46 |
47 | >
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/source/components/ui/IllustratedMessage.tsx:
--------------------------------------------------------------------------------
1 | import emoji from 'react-easy-emoji'
2 |
3 | export default ({
4 | emoji: e,
5 | message,
6 | inline,
7 | image,
8 | width,
9 | direction,
10 | backgroundcolor,
11 | }) => (
12 |
30 | {e ? (
31 |
38 | {emoji(e)}
39 |
40 | ) : (
41 |
47 | )}
48 |
49 |
54 | {message}
55 |
56 |
57 | )
58 |
--------------------------------------------------------------------------------
/source/components/ui/InfoBulle.css:
--------------------------------------------------------------------------------
1 | .info-bulle__interrogation-mark {
2 | color: var(--color);
3 | border: 1px solid var(--color);
4 | display: inline-block;
5 | font-weight: bold;
6 | user-select: none;
7 | font-size: 75%;
8 | width: 2.8ex;
9 | line-height: initial;
10 | border-radius: 50%;
11 | /* margin-right: 0.2em;
12 | margin-bottom: 0.5em; */
13 | padding: 1px;
14 | text-align: center;
15 | text-decoration: none;
16 | }
17 | .info-bulle__interrogation-mark:focus {
18 | outline: 1px dotted var(--darkColor);
19 | }
20 | .info-bulle__text {
21 | text-align: left;
22 | position: absolute;
23 | line-height: 1.2rem !important;
24 | z-index: -1;
25 | /* border: 1px solid var(--lighterColor); */
26 | padding: 0.4rem;
27 | min-width: 10rem;
28 | font-weight: normal;
29 | display: block;
30 | border-radius: 3px;
31 | background-color: white;
32 | transition: opacity 0.2s, transform 0.2s;
33 | opacity: 0;
34 | box-shadow: 0px 2px 4px -1px rgba(41, 117, 209, 0.2),
35 | 0px 4px 5px 0px rgba(41, 117, 209, 0.14),
36 | 0px 1px 10px 0px rgba(41, 117, 209, 0.12);
37 | pointer-events: none;
38 | transform: translate(2.8ex, -5px);
39 | }
40 |
41 | .info-bulle__interrogation-mark:hover + .info-bulle__text,
42 | .info-bulle__interrogation-mark:focus + .info-bulle__text {
43 | transform: translate(2.8ex, 1px);
44 | z-index: 1;
45 | opacity: 1;
46 | }
47 | .info-bulle__interrogation-mark:focus {
48 | position: relative;
49 | }
50 |
--------------------------------------------------------------------------------
/source/components/ui/InfoBulle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './InfoBulle.css'
3 |
4 | export default function InfoBulle({ children }: { children: React.ReactNode }) {
5 | return (
6 |
7 |
8 | ?
9 |
10 | {children}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/source/components/ui/NeutralH1.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const NeutralH1 = styled.h1`
4 | font-size: 100%;
5 | color: inherit;
6 | font-weight: normal;
7 | margin: 0;
8 | display: inline;
9 | line-height: 1rem;
10 | `
11 |
12 | export default NeutralH1
13 |
--------------------------------------------------------------------------------
/source/components/ui/Progress.css:
--------------------------------------------------------------------------------
1 | .progress__container {
2 | height: 0.6rem;
3 | background-color: var(--lighterColor);
4 | overflow: hidden;
5 | }
6 | .progress__bar {
7 | height: 100%;
8 | transition: width 0.2s;
9 | min-width: 6px;
10 | background-color: var(--color);
11 | }
12 |
--------------------------------------------------------------------------------
/source/components/ui/Progress.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Progress.css'
3 |
4 | type ProgressProps = {
5 | progress: number
6 | style?: React.CSSProperties
7 | className: string
8 | label: string
9 | }
10 |
11 | export default function Progress({
12 | progress,
13 | style,
14 | className,
15 | label,
16 | }: ProgressProps) {
17 | return (
18 |
19 |
26 |
{progress * 100}%
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/source/components/ui/RangeSlider.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import styled from 'styled-components'
3 | import { debounce } from '../../utils'
4 |
5 | type RangeSliderProps = {
6 | value?: number
7 | onChange: React.ChangeEventHandler
8 | }
9 |
10 | export default function RangeSlider({ value, onChange }: RangeSliderProps) {
11 | const debouncedOnChange = useCallback(debounce(100, onChange), [])
12 |
13 | return (
14 | debouncedOnChange(evt.target.value)}
20 | />
21 | )
22 | }
23 |
24 | const Input = styled.input`
25 | width: 100%;
26 | height: 15px;
27 | border-radius: 5px;
28 | background: #d3d3d3;
29 | outline: none;
30 | opacity: 0.7;
31 | transition: opacity 0.2s;
32 |
33 | &:hover {
34 | opacity: 1;
35 | }
36 |
37 | &::-webkit-slider-thumb {
38 | appearance: none;
39 | width: 25px;
40 | height: 25px;
41 | border-radius: 50%;
42 | background: var(--color);
43 | cursor: pointer;
44 | }
45 |
46 | &::-moz-range-thumb {
47 | width: 25px;
48 | height: 25px;
49 | border-radius: 50%;
50 | background: var(--color);
51 | cursor: pointer;
52 | }
53 | `
54 |
--------------------------------------------------------------------------------
/source/components/ui/WarningBlock.tsx:
--------------------------------------------------------------------------------
1 | import { usePersistingState } from 'Components/utils/persistState'
2 | import { ReactNode } from 'react'
3 | import { Trans } from 'react-i18next'
4 |
5 | type WarningProps = {
6 | localStorageKey: string
7 | children: ReactNode
8 | }
9 |
10 | export default function Warning({ localStorageKey, children }: WarningProps) {
11 | const [folded, fold] = usePersistingState(localStorageKey, false)
12 | return (
13 |
18 |
19 |
20 |
21 | 🚩 Avant de commencer...
22 |
23 | {' '}
24 | {folded && (
25 | fold(false)}
28 | >
29 |
30 | Lire les précisions
31 |
32 |
33 | )}
34 |
35 | {!folded && (
36 |
40 | {children}
41 |
42 | fold(true)}
45 | >
46 | J'ai compris
47 |
48 |
49 |
50 | )}
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/source/components/ui/animate.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 |
3 | export const appear = ({ children, delay = 0 }: Props) => (
4 |
10 | {children}
11 |
12 | )
13 |
14 | // Todo : better animate with fromRight on desktop
15 | export const fromBottom = ({ children, delay = 0 }: Props) => (
16 |
22 | {children}
23 |
24 | )
25 | export const fromTop = ({ children, delay = 0, duration }: Props) => (
26 |
32 | {children}
33 |
34 | )
35 |
36 | export default {
37 | appear,
38 | fromBottom,
39 | fromTop,
40 | }
41 |
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Bold.woff
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Bold.woff2
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-ExtraBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-ExtraBold.woff
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-ExtraBold.woff2
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Light.woff
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Light.woff2
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Medium.woff
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Medium.woff2
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Regular.woff
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Regular.woff2
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Thin.woff
--------------------------------------------------------------------------------
/source/components/ui/fonts/Marianne-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/components/ui/fonts/Marianne-Thin.woff2
--------------------------------------------------------------------------------
/source/components/ui/reset.css:
--------------------------------------------------------------------------------
1 | /*
2 | Unify browser styles and reset opinionated defaults;
3 | Reset style decisions that would break (rather than just customize)
4 | the widget's form layout.
5 | */
6 |
7 | html {
8 | box-sizing: border-box;
9 | }
10 | *,
11 | *:before,
12 | *:after {
13 | box-sizing: inherit;
14 | }
15 |
16 | body {
17 | margin: 0;
18 | }
19 | [hidden],
20 | .js-only {
21 | display: none;
22 | }
23 |
24 | /* Reset fieldset style */
25 | fieldset {
26 | border: 0;
27 | padding: 0;
28 | padding-top: 0.01em;
29 | margin: 0;
30 | min-width: 0;
31 | }
32 |
33 | /* Remove spinner controls from Firefox */
34 | input[type='number'] {
35 | appearance: textfield;
36 | }
37 |
38 | /* Remove spinner controls from Chrome, Safari, Edge, Opera */
39 | input[type=number]::-webkit-inner-spin-button,
40 | input[type=number]::-webkit-outer-spin-button {
41 | -webkit-appearance: none;
42 | margin: 0;
43 | }
44 |
45 | select {
46 | width: auto;
47 | height: auto;
48 | }
49 |
50 | input {
51 | line-height: normal;
52 | height: auto;
53 | }
54 |
55 | label {
56 | font-size: 100%;
57 | font-weight: normal;
58 | }
59 |
60 | button {
61 | background: none;
62 | border: 1px solid #222;
63 | border-radius: 0.2em;
64 | padding: 0 1em;
65 | }
66 |
67 | button:enabled {
68 | cursor: pointer;
69 | }
70 |
--------------------------------------------------------------------------------
/source/components/useBranchData.ts:
--------------------------------------------------------------------------------
1 | import { AppState } from '@/reducers/rootReducer'
2 | import { useEffect } from 'react'
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | export type BranchData = {
6 | deployURL: string
7 | pullRequestNumber?: string
8 | loaded: boolean
9 | }
10 |
11 | export default () => {
12 | const dispatch = useDispatch()
13 | const urlParams = new URLSearchParams(window.location.search)
14 |
15 | const searchPR = urlParams.get('PR')
16 |
17 | const pullRequestNumber = useSelector(
18 | (state: AppState) => state.pullRequestNumber
19 | )
20 |
21 | const setPullRequestNumber = (number) =>
22 | dispatch({ type: 'SET_PULL_REQUEST_NUMBER', number })
23 |
24 | useEffect(() => {
25 | if (pullRequestNumber) return
26 | if (searchPR) {
27 | setPullRequestNumber(searchPR)
28 | return
29 | }
30 | }, [searchPR, pullRequestNumber])
31 |
32 | const deployURL = pullRequestNumber
33 | ? `https://deploy-preview-${pullRequestNumber}--ecolab-data.netlify.app`
34 | : process.env.NODE_ENV === 'development'
35 | ? 'http://localhost:8081'
36 | : 'https://data.nosgestesclimat.fr'
37 |
38 | // rules are loaded from data.nosgestesclimat.fr since 26th february 2023, but PR cannot
39 |
40 | const loaded = pullRequestNumber !== undefined
41 |
42 | return {
43 | deployURL,
44 | pullRequestNumber,
45 | loaded,
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/source/components/useFetchDocumentation.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import useBranchData from './useBranchData'
3 |
4 | export default () => {
5 | const [data, setData] = useState()
6 | const branchData = useBranchData()
7 |
8 | useEffect(() => {
9 | if (!branchData.loaded) return
10 | if (process.env.NODE_ENV === 'development') {
11 | setData(require('../../nosgestesclimat/public/contenu-ecrit.json'))
12 | } else {
13 | fetch(branchData.deployURL + '/contenu-ecrit.json', {
14 | mode: 'cors',
15 | })
16 | .then((response) => response.json())
17 | .then((json) => {
18 | setData(json)
19 | })
20 | }
21 | }, [branchData.deployURL, branchData.loaded])
22 |
23 | return data
24 | }
25 |
--------------------------------------------------------------------------------
/source/components/utils/AutoCanonicalTag.tsx:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet'
2 |
3 | export default function AutoCanonicalTag({
4 | overrideHref,
5 | }: {
6 | overrideHref?: string
7 | }) {
8 | return (
9 |
10 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/source/components/utils/DisableScroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | export default function DisableScroll() {
4 | useEffect(() => {
5 | document.documentElement.style.overflow = 'hidden'
6 | return () => {
7 | document.documentElement.style.overflow = ''
8 | }
9 | }, [])
10 | return null
11 | }
12 |
--------------------------------------------------------------------------------
/source/components/utils/Emoji.tsx:
--------------------------------------------------------------------------------
1 | import emojiFn from 'react-easy-emoji'
2 |
3 | type PropType = {
4 | emoji: string
5 | }
6 |
7 | export default function Emoji({ emoji }: PropType) {
8 | return emojiFn(emoji)
9 | }
10 |
--------------------------------------------------------------------------------
/source/components/utils/NewTabSvg.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Svg = styled.svg`
4 | display: inline-block;
5 | width: 0.75em;
6 | height: auto;
7 | margin-left: 0.3em;
8 | path {
9 | fill: var(--color);
10 | }
11 | `
12 |
13 | export default function () {
14 | return (
15 |
16 |
22 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/source/components/utils/SitePathsContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 | import { SitePathsType } from 'sites/mon-entreprise.fr/sitePaths'
3 |
4 | export const SitePathsContext = createContext(
5 | {} as SitePathsType
6 | )
7 |
8 | export const SitePathProvider = SitePathsContext.Provider
9 |
10 | export type SitePaths = SitePathsType
11 |
--------------------------------------------------------------------------------
/source/components/utils/embeddedContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 | export const IsEmbeddedContext = createContext(false)
3 |
--------------------------------------------------------------------------------
/source/components/utils/embeddedContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 | export const IsEmbeddedContext = createContext(false)
3 |
--------------------------------------------------------------------------------
/source/components/utils/formatDataForDB.ts:
--------------------------------------------------------------------------------
1 | import { Simulation } from '@/types/simulation'
2 |
3 | type SimulationFormatted = {
4 | [key: string]: any
5 | }
6 |
7 | /**
8 | * Formats the simulation data, removing the ' . ' from the keys
9 | * @param simulation
10 | * @returns
11 | */
12 | export const formatDataForDB = (
13 | simulation: Simulation
14 | ): SimulationFormatted => {
15 | const simulationFormatted = { ...simulation }
16 |
17 | return Object.entries(
18 | simulationFormatted.situation as { [key: string]: any }
19 | ).reduce((acc: { [key: string]: any }, [key, value]: [string, any]) => {
20 | acc[key.replaceAll(' . ', '_').replaceAll(' ', '-')] = value
21 | return acc
22 | }, {})
23 | }
24 |
25 | export const reformateDataFromDB = (
26 | simulation: Simulation
27 | ): SimulationFormatted => {
28 | const simulationFormatted = { ...simulation }
29 |
30 | if (!simulationFormatted.situation) return simulationFormatted
31 |
32 | return Object.entries(
33 | simulationFormatted.situation as { [key: string]: any }
34 | ).reduce((acc: { [key: string]: any }, [key, value]: [string, any]) => {
35 | acc[key.replaceAll('_', ' . ').replaceAll('-', ' ')] = value
36 | return acc
37 | }, {})
38 | }
39 |
--------------------------------------------------------------------------------
/source/components/utils/formatFloat.ts:
--------------------------------------------------------------------------------
1 | export const formatFloat = ({
2 | number,
3 | locale = 'fr-FR',
4 | maximumFractionDigits = 2,
5 | }) => {
6 | return number.toLocaleString(locale, {
7 | minimumFractionDigits: 0,
8 | maximumFractionDigits,
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/source/components/utils/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export function useDebounce(value: T, delay: number) {
4 | const [debouncedValue, setDebouncedValue] = useState(value)
5 | useEffect(
6 | () => {
7 | // Update debounced value after delay
8 | const handler = setTimeout(() => {
9 | setDebouncedValue(value)
10 | }, delay)
11 |
12 | // Cancel the timeout if value changes (also on delay change or unmount)
13 | // This is how we prevent debounced value from updating if value is changed ...
14 | // .. within the delay period. Timeout gets cleared and restarted.
15 | return () => {
16 | clearTimeout(handler)
17 | }
18 | },
19 | [value, delay] // Only re-call effect if value or delay changes
20 | )
21 | return debouncedValue
22 | }
23 |
--------------------------------------------------------------------------------
/source/components/utils/toCSV.ts:
--------------------------------------------------------------------------------
1 | export default (header, jsonList) => {
2 | const lines = jsonList.map((data) =>
3 | header.map((headerKey) => data[headerKey])
4 | )
5 | const csv = [separate(header), ...lines.map((list) => separate(list))].join(
6 | '\r\n'
7 | )
8 | return csv
9 | }
10 | const guillemet = '"'
11 | const separate = (line) =>
12 | guillemet + line.join(`${guillemet};${guillemet}`) + guillemet
13 |
--------------------------------------------------------------------------------
/source/constants/groupNames.ts:
--------------------------------------------------------------------------------
1 | export const GROUP_NAMES = [
2 | {
3 | emoji: '🍋',
4 | name: 'Les citrons givrés',
5 | },
6 | {
7 | emoji: '🍊',
8 | name: 'Les oranges pressées',
9 | },
10 | {
11 | emoji: '🍉',
12 | name: 'Les pastèques aztèques',
13 | },
14 | {
15 | emoji: '🥥',
16 | name: 'Les Cocos Rico',
17 | },
18 | {
19 | emoji: '🥝',
20 | name: "Les Dai'kiwis",
21 | },
22 | {
23 | emoji: '🥦',
24 | name: 'Les brocolis suspects',
25 | },
26 | {
27 | emoji: '🥕',
28 | name: 'Les carottes glaciaires',
29 | },
30 | {
31 | emoji: '🍇',
32 | name: 'Les raisins zinzins',
33 | },
34 | {
35 | emoji: '🍎',
36 | name: 'Les pommes-pommes girls',
37 | },
38 | {
39 | emoji: '🍒',
40 | name: 'Les pussy griottes',
41 | },
42 | {
43 | emoji: '🍈',
44 | name: 'Les melons malins',
45 | },
46 | {
47 | emoji: '🌽',
48 | name: 'Les Épis Curieux',
49 | },
50 | {
51 | emoji: '🍍',
52 | name: 'Les super (a)nanas',
53 | },
54 | {
55 | emoji: '🍐',
56 | name: 'Les bonnes poires',
57 | },
58 | {
59 | emoji: '🍌',
60 | name: 'Les bana-nazes',
61 | },
62 | ]
63 |
--------------------------------------------------------------------------------
/source/constants/urls.ts:
--------------------------------------------------------------------------------
1 | export const NETLIFY_FUNCTIONS_URL =
2 | process.env.NODE_ENV === 'development'
3 | ? 'http://localhost:8888/.netlify/functions'
4 | : '/.netlify/functions'
5 |
6 | export const secure = process.env.NODE_ENV === 'development' ? '' : 's'
7 | const protocol = `http${secure}://`
8 |
9 | export const SERVER_URL = protocol + process.env.SERVER_URL
10 |
11 | export const SURVEYS_URL = SERVER_URL + '/surveys/'
12 |
13 | export const GROUP_URL =
14 | (process.env.NODE_ENV === 'development'
15 | ? 'http://localhost:3000'
16 | : SERVER_URL) + '/group'
17 |
--------------------------------------------------------------------------------
/source/global.css:
--------------------------------------------------------------------------------
1 | @tailwind utilities;
2 |
3 | * {
4 | font-family: 'Marianne', Arial, sans-serif;
5 | }
6 |
7 | h1 {
8 | @apply text-2xl;
9 | @apply font-bold mb-1;
10 | @apply mb-4;
11 | }
12 |
13 | h2 {
14 | @apply text-xl;
15 | @apply mb-4;
16 | }
17 |
18 | h3 {
19 | @apply mb-2;
20 | @apply text-lg;
21 | }
22 |
23 | h4 {
24 | @apply mb-2;
25 | @apply text-base;
26 | }
27 |
28 | h5 {
29 | @apply mb-2;
30 | @apply text-base;
31 | }
32 |
33 | h6 {
34 | @apply mb-2;
35 | @apply text-base;
36 | }
37 |
38 | h1,
39 | h2,
40 | h3,
41 | h4,
42 | h5,
43 | h6 {
44 | @apply mt-0;
45 | @apply text-title;
46 | @apply font-semibold;
47 | @apply scroll-mt-4; /* Add a margin for anchor links */
48 | }
49 |
--------------------------------------------------------------------------------
/source/hooks/useDisplayOnIntersecting.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react'
2 |
3 | export default function({
4 | root = null,
5 | rootMargin,
6 | threshold = 0,
7 | unobserve = true
8 | }: IntersectionObserverInit & { unobserve?: boolean }): [
9 | React.RefObject,
10 | boolean
11 | ] {
12 | const ref = useRef(null)
13 | const [wasOnScreen, setWasOnScreen] = useState(false)
14 |
15 | useEffect(() => {
16 | const observer = new IntersectionObserver(
17 | ([entry]) => {
18 | if (entry.isIntersecting) {
19 | setWasOnScreen(entry.isIntersecting)
20 | ref.current && unobserve && observer.unobserve(ref.current)
21 | }
22 | if (!entry.isIntersecting && !unobserve) {
23 | setWasOnScreen(entry.isIntersecting)
24 | }
25 | },
26 | {
27 | root,
28 | rootMargin,
29 | threshold
30 | }
31 | )
32 | const node = ref.current
33 | if (node) {
34 | observer.observe(node)
35 | }
36 | return () => {
37 | node && unobserve && observer.unobserve(node)
38 | }
39 | }, [root, rootMargin, threshold, ref.current])
40 |
41 | return [ref, wasOnScreen]
42 | }
43 |
--------------------------------------------------------------------------------
/source/hooks/useGetCurrentSimulation.ts:
--------------------------------------------------------------------------------
1 | import { AppState } from '@/reducers/rootReducer'
2 | import { getIsSimulationValid } from '@/utils/getIsSimulationValid'
3 | import { useSelector } from 'react-redux'
4 |
5 | export const useGetCurrentSimulation = () => {
6 | const currentSimulation = useSelector((state: AppState) => state.simulation)
7 | const isCurrentSimulationValid = getIsSimulationValid(currentSimulation)
8 |
9 | return isCurrentSimulationValid ? currentSimulation : null
10 | }
11 |
--------------------------------------------------------------------------------
/source/hooks/useKeyPress.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | /**
3 | * useKeyPress
4 | * @param {string} key - the name of the key to respond to, compared against event.key
5 | * @param {function} action - the action to perform on key press
6 | */
7 | export default function useKeypress(
8 | key: string,
9 | control: boolean,
10 | action: Function,
11 | eventType = 'keyup',
12 | hookConditions
13 | ) {
14 | useEffect(() => {
15 | function onKeyup(e) {
16 | if (e.key === key && (!control || e.ctrlKey)) action(e)
17 | }
18 | window.addEventListener(eventType, onKeyup)
19 | return () => window.removeEventListener(eventType, onKeyup)
20 | }, hookConditions)
21 | }
22 |
--------------------------------------------------------------------------------
/source/hooks/useMediaQuery.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | function useMediaQuery(query: string): boolean {
4 | const getMatches = (query: string): boolean => {
5 | // Prevents SSR issues
6 |
7 | if (typeof window !== 'undefined') {
8 | return window.matchMedia(query).matches
9 | }
10 |
11 | return false
12 | }
13 |
14 | const [matches, setMatches] = useState(getMatches(query))
15 |
16 | function handleChange() {
17 | setMatches(getMatches(query))
18 | }
19 |
20 | useEffect(() => {
21 | const matchMedia = window.matchMedia(query)
22 |
23 | // Triggered at the first client-side load and if query changes
24 |
25 | handleChange()
26 |
27 | // Listen matchMedia
28 |
29 | if (matchMedia.addListener) {
30 | matchMedia.addListener(handleChange)
31 | } else {
32 | matchMedia.addEventListener('change', handleChange)
33 | }
34 |
35 | return () => {
36 | if (matchMedia.removeListener) {
37 | matchMedia.removeListener(handleChange)
38 | } else {
39 | matchMedia.removeEventListener('change', handleChange)
40 | }
41 | }
42 |
43 | // eslint-disable-next-line react-hooks/exhaustive-deps
44 | }, [query])
45 |
46 | return matches
47 | }
48 |
49 | export default useMediaQuery
50 |
--------------------------------------------------------------------------------
/source/hooks/useSetUserId.ts:
--------------------------------------------------------------------------------
1 | import { setUserId } from '@/actions/actions'
2 | import { AppState } from '@/reducers/rootReducer'
3 | import { useEffect } from 'react'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import { v4 as uuid } from 'uuid'
6 |
7 | export const useSetUserId = () => {
8 | const dispatch = useDispatch()
9 |
10 | const userId = useSelector((state: AppState) => state.user.userId)
11 |
12 | useEffect(() => {
13 | if (!userId) {
14 | dispatch(setUserId(uuid()))
15 | }
16 | }, [userId, dispatch])
17 | }
18 |
--------------------------------------------------------------------------------
/source/images/1F1FA-1F1E6.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/source/images/1F464.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/source/images/1F465.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/source/images/26D4.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/source/images/CO2e.tsx:
--------------------------------------------------------------------------------
1 | function SvgCo2E(props) {
2 | return (
3 |
4 |
5 |
6 |
7 |
19 |
30 |
31 | {'CO\u2082e'}
32 |
33 |
34 |
35 |
36 | )
37 | }
38 |
39 | export default SvgCo2E
40 |
--------------------------------------------------------------------------------
/source/images/Logo.tsx:
--------------------------------------------------------------------------------
1 | function SvgLogoNgcGithub(props) {
2 | return (
3 |
10 |
14 |
15 |
21 |
22 | )
23 | }
24 |
25 | export default SvgLogoNgcGithub
26 |
--------------------------------------------------------------------------------
/source/images/alexandre-lecocq-climatiseurs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/alexandre-lecocq-climatiseurs.jpg
--------------------------------------------------------------------------------
/source/images/arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/source/images/burger-menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/source/images/dessin-nosgestesclimat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/dessin-nosgestesclimat.png
--------------------------------------------------------------------------------
/source/images/ecolab-climat-dessin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/ecolab-climat-dessin.png
--------------------------------------------------------------------------------
/source/images/exemple-contexte.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/exemple-contexte.png
--------------------------------------------------------------------------------
/source/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/favicon.png
--------------------------------------------------------------------------------
/source/images/filtre.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | filter_list
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/source/images/illustration-micmac.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/illustration-micmac.png
--------------------------------------------------------------------------------
/source/images/international-illustration.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/international-illustration.jpeg
--------------------------------------------------------------------------------
/source/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/logo.png
--------------------------------------------------------------------------------
/source/images/map-directions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/map-directions.png
--------------------------------------------------------------------------------
/source/images/matt-benson-chien-baignade.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/matt-benson-chien-baignade.jpg
--------------------------------------------------------------------------------
/source/images/nosgestesclimat.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/nosgestesclimat.webp
--------------------------------------------------------------------------------
/source/images/petit-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/petit-logo.png
--------------------------------------------------------------------------------
/source/images/petit-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/petit-logo@2x.png
--------------------------------------------------------------------------------
/source/images/petit-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/petit-logo@3x.png
--------------------------------------------------------------------------------
/source/images/pexels-helena-lopes-4409455.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/pexels-helena-lopes-4409455.jpg
--------------------------------------------------------------------------------
/source/images/priscilla-du-preez-rollercoaster.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/priscilla-du-preez-rollercoaster.jpg
--------------------------------------------------------------------------------
/source/images/publicodes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/publicodes.png
--------------------------------------------------------------------------------
/source/images/ted-bryan-yu-montagnes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/ted-bryan-yu-montagnes.jpg
--------------------------------------------------------------------------------
/source/images/transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/transparent.png
--------------------------------------------------------------------------------
/source/images/union-européenne.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | European flag
4 |
5 |
6 |
--------------------------------------------------------------------------------
/source/images/william-bossen-fonte-glaces.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/images/william-bossen-fonte-glaces.jpg
--------------------------------------------------------------------------------
/source/locales/.gitignore:
--------------------------------------------------------------------------------
1 | static-analysis-fr.json
--------------------------------------------------------------------------------
/source/locales/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next'
2 | import { initReactI18next } from 'react-i18next'
3 | import { getLangInfos, Lang } from './translation'
4 | import unitsTranslations from './units.yaml'
5 |
6 | i18next
7 | .use(initReactI18next)
8 | .init({
9 | fallbackLng: getLangInfos(Lang.Default).abrv,
10 | resources: Object.fromEntries(
11 | Object.keys(Lang)
12 | .filter((key) => key !== 'Default')
13 | .flatMap((key) => {
14 | const lng = key.toLowerCase()
15 | return [
16 | [lng, { units: unitsTranslations[lng] }],
17 | // [lng, { categories: categoriesTranslations[lng] }],
18 | ]
19 | })
20 | ),
21 | react: {
22 | useSuspense: false,
23 | },
24 | })
25 | .catch((err) => console?.error('Error from i18n load', err))
26 |
27 | export default i18next
28 |
--------------------------------------------------------------------------------
/source/locales/pages/en/budgetBottom.md:
--------------------------------------------------------------------------------
1 | ### Description of cost categories
2 |
3 | - **Development, deployment, product, design 👨💻**
4 |
5 | Development, product, deployment and design costs represent the vast majority of our budget.
6 | We are a small team of 9 freelancers, multi-disciplinary in technical, strategic and business aspects
7 | technical, strategic and business aspects.
8 |
9 | - **Software and hosting 💻**
10 |
11 | Our open-source model gives us free access to the majority of the tools
12 | tools we use (code hosting, test servers, etc.). The
13 | site is hosted on Netlify .
14 |
15 | > #### About VAT
16 | >
17 | > Unlike private-sector companies, public-sector bodies are not entitled to
18 | > VAT on purchases made in the course of their business
19 | > activity.
20 | >
21 | > The amount including VAT includes VAT at the rate of 20%.
22 | >
23 | > VAT is collected and paid back to the government, reducing the budget available for > the project
24 | > budget available for the project.
25 |
--------------------------------------------------------------------------------
/source/locales/pages/en/budgetTop.md:
--------------------------------------------------------------------------------
1 | # Budget
2 |
3 | **Nos Gestes Climat** is a digital public service, which is why we are
4 | transparent about the resources we allocate and how they are used
5 | used.
6 |
7 | ## Principles
8 |
9 | We follow the beta.gouv manifesto
10 | manifesto, the principles of which can be found here:
11 |
12 | > - The needs of users take precedence over the needs of the administration
13 | > - The team's management style is based on trust
14 | > - The team adopts an iterative, continuous improvement approach
15 |
16 | ## How it works
17 |
18 | Nos Gestes Climat is a state-owned start-up: the team is therefore led by an intrapreneur who is responsible for the digital service developed within his administration (ADEME in this case).
19 |
20 | His role is multifaceted: deployment, product management, reporting to his administration (budget, progress reports).
21 |
22 | The budget shown here does not include the intrapreneur, since he is an ADEME employee, but concerns the team members.
23 |
24 | ## Budget consumed
25 |
--------------------------------------------------------------------------------
/source/locales/pages/en/méthode.md:
--------------------------------------------------------------------------------
1 | # Understanding our calculations and advice
2 |
3 | Our calculation model is completely transparent.
4 |
5 | 
6 |
7 | At any time during the simulation, click on "🔬 understand the
8 | calculation" and follow the links.
9 |
10 | The code is not only transparent, but also open to contributions: everyone can
11 | explore it, give feedback, improve it.
12 |
13 | [🎤 Come and contribute](/contact)!
14 |
15 | ## Deepen your knowledge of the actions
16 |
17 | In addition to the interactive pathway to action, you can delve deeper
18 | into each of the issues by browsing the full fact sheets that will give
19 | you the context of each action, key figures and infographics, the
20 | sources that led to our calculations, etc.
21 |
22 | [Discover all the detailed actions](/actions/plus)!
23 |
--------------------------------------------------------------------------------
/source/locales/pages/es/méthode.md:
--------------------------------------------------------------------------------
1 | # Comprender nuestros cálculos y consejos
2 |
3 | Nuestro modelo de cálculo es totalmente transparente.
4 |
5 | 
6 |
7 | En cualquier momento de la simulación, haz clic en "🔬 entender el
8 | cálculo" y sigue los enlaces.
9 |
10 | El código no sólo es transparente, sino también contributivo: todo el
11 | mundo puede explorarlo, dar su opinión y mejorarlo.
12 |
13 | [🎤 ¡Ven y contribuye](/contribuer)!
14 |
15 | ## Profundizar en el conocimiento de las acciones
16 |
17 | Además del camino interactivo hacia la acción, puede profundizar en cada
18 | uno de los temas consultando las fichas completas que le darán el
19 | contexto de cada acción, las cifras clave y la infografía, las fuentes
20 | que nos han llevado a realizar nuestros cálculos, etc.
21 |
22 | ¡[Descubra todas las acciones detalladas](/actions/plus)!
23 |
--------------------------------------------------------------------------------
/source/locales/pages/fr/budgetBottom.md:
--------------------------------------------------------------------------------
1 | ### Description des catégories de coût
2 |
3 | - **Développement, déploiement, produit, design 👨💻**
4 |
5 | Les coûts de développement, produit, déploiement et design représentent la grande majorité de notre budget.
6 | Nous sommes une petite équipe de 9 freelances, pluridisciplinaires aussi bien
7 | sur les aspects techniques, stratégiques et métiers.
8 |
9 | - **Logiciels et hébergement 💻**
10 |
11 | Notre modèle open-source nous permet d’accéder gratuitement à la majorité des
12 | outils que nous utilisons (hébergement de code, serveurs de tests, etc.). Le
13 | site est hébergé sur [Netlify](https://www.netlify.com).
14 |
15 | > #### À propos de la TVA
16 | >
17 | > Contrairement aux entreprises du secteur privé, les administrations ne peuvent
18 | > pas récupérer la TVA supportée sur leurs achats dans le cadre de leur
19 | > activité.
20 | >
21 | > Le montant TTC inclut la TVA au taux de 20%.
22 | >
23 | > La TVA est collectée et reversée à l'État et diminue donc le montant du budget
24 | > utilisable sur le projet.
25 |
--------------------------------------------------------------------------------
/source/locales/pages/fr/budgetTop.md:
--------------------------------------------------------------------------------
1 | # Budget
2 |
3 | **Nos Gestes Climat** est un service public numérique, c’est pourquoi nous
4 | sommes transparents sur les ressources allouées et la manière dont elles sont
5 | employées.
6 |
7 | ## Principes
8 |
9 | Nous suivons le [manifeste beta.gouv](https://beta.gouv.fr/approche/manifeste)
10 | dont nous rappelons les principes ici :
11 |
12 | > - Les besoins des utilisateurs sont prioritaires sur les besoins de l’administration
13 | > - Le mode de gestion de l’équipe repose sur la confiance
14 | > - L’équipe adopte une approche itérative et d’amélioration en continu
15 |
16 | ## Fonctionnement
17 |
18 | Nos Gestes Climat est une start-up d'état : l'équipe est donc portée par un intrapreneur qui est responsable du service numérique développé au sein de son administration (l'ADEME en l'occurence).
19 |
20 | Son rôle est multiple : déploiement, gestion des produits, référent auprès de son administration (budget, compte rendus d'avancement).
21 |
22 | Le budget exposé ici ne prend pas en compte l'intrapreneur puisque qu'il est salarié de l'ADEME mais concerne les membres de l'équipe.
23 |
24 | ## Budget consommé
25 |
--------------------------------------------------------------------------------
/source/locales/pages/fr/méthode.md:
--------------------------------------------------------------------------------
1 | # Comprendre nos calculs et conseils
2 |
3 | Notre modèle de calcul est entièrement transparent.
4 |
5 | 
6 |
7 | À tout moment pendant la simulation, cliquez sur "🔬 comprendre le calcul" et suivez les liens.
8 |
9 | Le code est non seulement transparent, mais aussi contributif : chacun peut l'explorer, donner son avis, l'améliorer.
10 |
11 | [🎤 Venez contribuer](/contact) !
12 |
13 | ## Approfondissez votre connaissance des actions
14 |
15 | En plus du parcours interactif de passage à l'action, vous pouvez approfondir chacun des enjeux en parcourant les fiches complètes qui vous donneront le contexte de chaque action, les chiffres clés et infographies, les sources qui ont menés à nos calculs, etc.
16 |
17 | [Découvrez toutes les actions détaillées](/actions/plus) !
18 |
--------------------------------------------------------------------------------
/source/locales/pages/it/méthode.md:
--------------------------------------------------------------------------------
1 | # Comprendere i nostri calcoli e consigli
2 |
3 | Il nostro modello di calcolo è completamente trasparente.
4 |
5 | 
6 |
7 | In qualsiasi momento della simulazione, fare clic su "🔬 capire il
8 | calcolo" e seguire i link.
9 |
10 | Il codice non è solo trasparente, ma anche contributivo: tutti possono
11 | esplorarlo, fornire feedback, migliorarlo.
12 |
13 | [🎤 Venite a contribuire](/contribuer)!
14 |
15 | ## Approfondire la conoscenza delle azioni
16 |
17 | Oltre al percorso interattivo per l'azione, è possibile approfondire
18 | ogni questione consultando le schede complete che forniscono il contesto
19 | di ogni azione, le cifre chiave e le infografiche, le fonti che hanno
20 | portato ai nostri calcoli, ecc.
21 |
22 | [Scoprite tutte le azioni dettagliate](/actions/plus)!
23 |
--------------------------------------------------------------------------------
/source/légal/cgu.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/légal/cgu.docx
--------------------------------------------------------------------------------
/source/légal/confidentialité.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/légal/confidentialité.docx
--------------------------------------------------------------------------------
/source/pages/groupe-dashboard/components/Badge.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react'
2 |
3 | export default function Badge({
4 | children,
5 | className,
6 | }: { className?: string } & PropsWithChildren) {
7 | return (
8 |
11 | {children}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/source/pages/groupe-dashboard/components/ClassementMember.tsx:
--------------------------------------------------------------------------------
1 | import Badge from './Badge'
2 |
3 | export default function ClassementMember({
4 | rank,
5 | name,
6 | quantity,
7 | isTopThree,
8 | isCurrentMember,
9 | }: {
10 | rank: JSX.Element | string
11 | name: string
12 | quantity: JSX.Element | string
13 | isTopThree?: boolean
14 | isCurrentMember?: boolean
15 | }) {
16 | return (
17 |
18 |
19 |
20 | {rank}
21 |
22 | {name}
23 | {isCurrentMember && (
24 |
25 | Vous
26 |
27 | )}
28 |
29 | {quantity}
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/source/pages/groupe-dashboard/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import ButtonLink from '@/components/groupe/ButtonLink'
2 | import { Trans } from 'react-i18next'
3 |
4 | export default function Footer() {
5 | return (
6 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/source/pages/groupe-dashboard/components/PercentageDiff.tsx:
--------------------------------------------------------------------------------
1 | import { formatValue } from 'publicodes'
2 |
3 | export default function PercentageDiff({ variation }: { variation: number }) {
4 | return (
5 | 0
10 | ? 'text-red-600'
11 | : 'text-green-700'
12 | } text-xs`}
13 | >
14 | {Math.round(variation) === 0 ? '' : variation < 0 ? '' : '+'}
15 | {Math.round(variation) === 0
16 | ? '='
17 | : `${formatValue(variation, { precision: 0 })}%`}
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/source/pages/groupe-dashboard/components/SondagesBlock.tsx:
--------------------------------------------------------------------------------
1 | import Separator from '@/components/groupe/Separator'
2 | import { Link } from 'react-router-dom'
3 |
4 | export default function SondagesBlock() {
5 | return (
6 |
7 |
8 |
9 | Entreprises, collectivités, écoles
10 |
11 |
12 | Les sondages et conférences vous
13 | permettent de comparer votre empreinte en direct ou en groupes de plus
14 | de 20 personnes
15 |
16 |
17 | Commencer
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/source/pages/groupe-dashboard/utils/getTopThreeAndRestMembers.ts:
--------------------------------------------------------------------------------
1 | import { Member } from '@/types/groups'
2 |
3 | export const getTopThreeAndRestMembers = (members: Member[] = []) => {
4 | const sortedMembers = members.sort((memberA, memberB) => {
5 | const totalA = memberA?.results?.total
6 | const totalB = memberB?.results?.total
7 |
8 | return totalA !== undefined && totalA !== undefined
9 | ? parseFloat(totalA) - parseFloat(totalB)
10 | : -1
11 | })
12 |
13 |
14 | return sortedMembers.reduce(
15 | (acc, member, index) => {
16 | if (index < 3 && member?.results?.total !== undefined) {
17 | acc.topThreeMembers.push(member)
18 | } else {
19 | acc.restOfMembers.push(member)
20 | }
21 | return acc
22 | },
23 | { topThreeMembers: [], restOfMembers: [] } as {
24 | topThreeMembers: Member[]
25 | restOfMembers: Member[]
26 | }
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/source/pages/mes-groupes/components/CreateFirstGroupSection.tsx:
--------------------------------------------------------------------------------
1 | import ButtonLink from '@/components/groupe/ButtonLink'
2 | import Container from '@/components/groupe/Container'
3 | import { Trans } from 'react-i18next'
4 |
5 | export default function CreateFirstGroupSection() {
6 | return (
7 |
8 |
9 | Créez votre premier groupe
10 |
11 |
12 | Invitez vos proches pour comparer vos résultats. Cela prend{' '}
13 | 1 minute !
14 |
15 |
19 | Commencer
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/source/pages/mes-groupes/components/CreateOtherGroupsSection.tsx:
--------------------------------------------------------------------------------
1 | import ButtonLink from '@/components/groupe/ButtonLink'
2 | import Separator from '@/components/groupe/Separator'
3 | import { Group } from '@/types/groups'
4 | import { Trans } from 'react-i18next'
5 | import GroupList from './GroupList'
6 |
7 | export default function CreateOtherGroupsSection({
8 | groups,
9 | }: {
10 | groups: Group[]
11 | }) {
12 | return (
13 | <>
14 |
15 |
16 |
17 |
18 |
19 | Créez un autre groupe
20 |
21 |
22 |
23 | Vous pouvez créer un nouveau groupe avec d’autres amis.
24 |
25 |
26 |
31 | Créer un autre groupe
32 |
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/source/pages/mes-groupes/components/GroupList.tsx:
--------------------------------------------------------------------------------
1 | import GroupItem from './GroupItem'
2 |
3 | export default function GroupList({ groups, className = '' }) {
4 | return (
5 |
6 | {groups.map((group, index) => {
7 | return (
8 |
9 | )
10 | })}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/source/pages/mes-groupes/components/ServerErrorSection.tsx:
--------------------------------------------------------------------------------
1 | import Container from '@/components/groupe/Container'
2 | import { Trans } from 'react-i18next'
3 |
4 | export const ServerErrorSection = () => {
5 | return (
6 |
7 |
8 | Oups ! Désolé, une erreur est survenue.
9 |
10 |
11 |
12 | Nos équipes ont été prévenues ; veuillez réessayer d'accéder à cette
13 | page plus tard.
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/source/reducers/group/index.ts:
--------------------------------------------------------------------------------
1 | import { Group } from '@/types/groups'
2 |
3 | export const groupsReducer = (
4 | state = [],
5 | { type, group }: { type: string; group: Group }
6 | ) => {
7 | switch (type) {
8 | case 'ADD_GROUP':
9 | return [...state, group]
10 | case 'REMOVE_GROUP':
11 | return state.filter((g: Group) => g._id !== group._id)
12 | case 'UPDATE_GROUP':
13 | return state.map((g: Group) => (g._id === group._id ? group : g))
14 | default:
15 | return state
16 | }
17 | }
18 |
19 | export const groupToRedirectToReducer = (
20 | state = null,
21 | { type, group }: { type: string; group: Group }
22 | ) => {
23 | switch (type) {
24 | case 'SET_GROUP_TO_REDIRECT_TO':
25 | return group
26 | default:
27 | return state
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/source/reducers/storageReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '@/actions/actions'
2 | import { AppState } from '@/reducers/rootReducer'
3 | import { createStateFromSavedSimulation } from '@/selectors/storageSelectors'
4 |
5 | export default (state: AppState, action: Action): AppState => {
6 | switch (action.type) {
7 | case 'LOAD_PREVIOUS_SIMULATION': // todo : delete ? - used in sessionbar
8 | return {
9 | ...state,
10 | ...createStateFromSavedSimulation(state),
11 | }
12 | case 'DELETE_PREVIOUS_SIMULATION': // todo : delete
13 | return {
14 | ...state,
15 | previousSimulation: null,
16 | }
17 | default:
18 | return state
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/source/reducers/user/index.ts:
--------------------------------------------------------------------------------
1 | export const userReducer = (
2 | state = { userId: '', name: '', email: '' },
3 | {
4 | type,
5 | userId,
6 | name,
7 | email,
8 | }: { type: string; userId: string; name: string; email: string }
9 | ) => {
10 | switch (type) {
11 | case 'SET_USER_ID':
12 | return { ...state, userId }
13 |
14 | case 'SET_USER_NAME_AND_EMAIL':
15 | return { ...state, name, email }
16 |
17 | default:
18 | return state
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/source/selectors/groupSelectors.ts:
--------------------------------------------------------------------------------
1 | import { AppState } from '@/reducers/rootReducer'
2 |
3 | export const getGroups = (state: AppState) => state.groups
4 |
--------------------------------------------------------------------------------
/source/sites/publicodes/ActionConversation.tsx:
--------------------------------------------------------------------------------
1 | import { setSimulationConfig } from '@/actions/actions'
2 | import Simulation from '@/components/Simulation'
3 | import { useNextQuestions } from '@/hooks/useNextQuestion'
4 | import { AppState } from '@/reducers/rootReducer'
5 | import { questionConfig } from '@/sites/publicodes/questionConfig'
6 | import { useEffect } from 'react'
7 | import { useDispatch, useSelector } from 'react-redux'
8 |
9 | export default ({ dottedName }) => {
10 | const nextQuestions = useNextQuestions()
11 | const configSet = useSelector((state: AppState) => state.simulation?.config)
12 |
13 | // TODO here we need to apply a rustine to accommodate for this issue
14 | // https://github.com/betagouv/mon-entreprise/issues/1316#issuecomment-758833973
15 | // to be continued...
16 | const config = {
17 | objectifs: [dottedName],
18 | situation: { ...(configSet?.situation ?? {}) },
19 | questions: questionConfig,
20 | }
21 |
22 | const dispatch = useDispatch()
23 | useEffect(() => {
24 | dispatch(setSimulationConfig(config))
25 | }, [dottedName])
26 |
27 | if (!configSet) {
28 | return null
29 | }
30 |
31 | return nextQuestions.length > 0 ? (
32 | ,
37 | questionHeadingLevel: 3,
38 | isFromActionCard: true,
39 | }}
40 | />
41 | ) : null
42 | }
43 |
--------------------------------------------------------------------------------
/source/sites/publicodes/Actions.tsx:
--------------------------------------------------------------------------------
1 | import NorthstarBanner from '@/components/Feedback/NorthstarBanner'
2 | import Title from '@/components/groupe/Title'
3 | import Meta from '@/components/utils/Meta'
4 | import { ScrollToTop } from '@/components/utils/Scroll'
5 | import { Trans, useTranslation } from 'react-i18next'
6 | import { Route, Routes } from 'react-router-dom'
7 | import Action from './Action'
8 | import ActionPlus from './ActionPlus'
9 | import ActionsList from './ActionsList'
10 | import ListeActionPlus from './ListeActionPlus'
11 | import ScoreBar from './ScoreBar'
12 |
13 | export default () => {
14 | const { t } = useTranslation()
15 | return (
16 | <>
17 |
21 |
22 | Agir} />
23 |
24 |
25 |
26 | } />
27 | } />
28 |
29 | } />
30 | } />
31 |
32 | } />
33 |
34 | >
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/source/sites/publicodes/ActionsChosenIndicator.tsx:
--------------------------------------------------------------------------------
1 | import animate from 'Components/ui/animate'
2 | import { Trans, useTranslation } from 'react-i18next'
3 | import { useSelector } from 'react-redux'
4 |
5 | export default ({}) => {
6 | const { t } = useTranslation()
7 | const actionChoices = useSelector((state) => state.actionChoices),
8 | count = Object.values(actionChoices).filter((a) => a === true).length
9 |
10 | if (count == 0) {
11 | return '.'
12 | }
13 | return (
14 |
15 | ,{' '}
16 |
35 |
36 | {count}
37 | ✔
38 |
39 |
40 | sélectionnées .
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/source/sites/publicodes/DefaultFootprint.tsx:
--------------------------------------------------------------------------------
1 | import Engine from 'publicodes'
2 | import { useTranslation } from 'react-i18next'
3 | import { useSelector } from 'react-redux'
4 | import { getCurrentLangInfos } from '../../locales/translation'
5 | import { WithEngine } from '../../RulesProvider'
6 | import { humanWeight } from './HumanWeight'
7 |
8 | export const meanFormatter = ({ t, i18n }, value) =>
9 | humanWeight({ t, i18n }, value, false).join(' ')
10 |
11 | const DefaultFootprint = ({}) => {
12 | const rules = useSelector((state) => state.rules)
13 | const engine = new Engine(rules)
14 | const { t, i18n } = useTranslation()
15 | const currentLangInfos = getCurrentLangInfos(i18n)
16 |
17 | return (
18 |
19 | {meanFormatter({ t, i18n }, engine.evaluate('bilan').nodeValue)}
20 |
21 | )
22 | }
23 |
24 | export default () => (
25 |
26 |
27 |
28 | )
29 |
--------------------------------------------------------------------------------
/source/sites/publicodes/DocumentationButton.js:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { Link } from 'react-router-dom'
3 |
4 | const DocumentationButton = (props) => {
5 | return (
6 |
16 |
17 | Documentation
18 |
19 |
20 | )
21 | }
22 |
23 | export default DocumentationButton
24 |
--------------------------------------------------------------------------------
/source/sites/publicodes/LandingContent.tsx:
--------------------------------------------------------------------------------
1 | export default ({ children, background = false, footer = false }) => (
2 | div {
17 | margin: 0 auto;
18 | padding: 0 1rem;
19 | }
20 | p {
21 | max-width: 40rem;
22 | margin: 1rem auto;
23 | }
24 | background: ${background ? 'var(--lightestColor)' : 'none'};
25 | ${footer && 'margin-bottom: 0'}
26 | `}
27 | >
28 |
{children}
29 |
30 | )
31 |
--------------------------------------------------------------------------------
/source/sites/publicodes/MetricFilters.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { useSearchParams } from 'react-router-dom'
3 |
4 | export default ({ selected }) => {
5 | const [searchParams, setSearchParams] = useSearchParams()
6 | const { métrique, ...otherSearchParams } = searchParams
7 |
8 | return (
9 |
46 | setSearchParams({
47 | otherSearchParams,
48 | ...(selected ? {} : { métrique: 'pétrole' }),
49 | })
50 | }
51 | >
52 |
53 | Réduire ma conso de pétrole
54 |
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/source/sites/publicodes/ModelIssuePreviews.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { Markdown } from '../../components/utils/markdown'
3 |
4 | const labelString = ['exposé'].join(',')
5 | export default () => {
6 | const [issues, setIssues] = useState([])
7 |
8 | useEffect(() => {
9 | fetch(
10 | `https://api.github.com/repos/datagir/nosgestesclimat/issues?labels=${labelString}`
11 | )
12 | .then((res) => res.json())
13 | .then(setIssues)
14 | .catch((e) => console.log(e))
15 | }, [])
16 |
17 | if (!issues.length) return Chargement en cours...
18 | return (
19 |
31 | {issues.map(({ body, id, html_url: url, title }) => (
32 |
33 | {title}
34 |
46 | {body}
47 |
48 | En savoir plus
49 |
50 | ))}
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/source/sites/publicodes/ModelStatsBlock.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { useSelector } from 'react-redux'
3 | import { Link } from 'react-router-dom'
4 |
5 | const Loading = () => Chargement du modèle...
6 | export default () => {
7 | const rules = useSelector((state) => state.rules)
8 | if (!rules) return
9 | const numberOfRules = Object.keys(rules).length
10 | const numberOfQuestions = Object.values(rules).filter(
11 | (el) => el && el.question
12 | ).length
13 |
14 | const NumberOfRules = () => {numberOfRules}
15 | const NumberOfQuestions = () => {numberOfQuestions}
16 | return (
17 |
18 |
19 |
20 | Le modèle comprend aujourd'hui règles de calcul.
21 | Parmi elles, règles sont des questions à poser à
22 | l'utilisateur pour calculer un résultat précis.
23 |
24 |
25 |
26 |
27 | Découvrez{' '}
28 |
29 | la liste des questions disponibles dans le modèle
30 |
31 | .
32 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/source/sites/publicodes/Search.js:
--------------------------------------------------------------------------------
1 | import emoji from 'react-easy-emoji'
2 |
3 | export default ({ setInput, input }) => (
4 |
10 | {
22 | setInput(event.target.value)
23 | }}
24 | />
25 |
34 | {emoji('🔍')}
35 |
36 |
37 | )
38 |
--------------------------------------------------------------------------------
/source/sites/publicodes/SimulationMissing.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { Link } from 'react-router-dom'
3 |
4 | export default () => {
5 | return (
6 |
14 |
19 | 🔒{' '}
20 |
21 | Pour débloquer ce parcours, vous devez d'abord terminer le test.
22 |
23 |
24 |
25 |
26 | Faire le test
27 |
28 |
29 |
30 |
31 |
32 | Vous pouvez aussi continuer avec un{' '}
33 | profil type.
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/source/sites/publicodes/SkipLinks.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { Link } from 'react-router-dom'
3 |
4 | export default () => {
5 | const focusTarget = (focusTarget) => {
6 | const target = document.querySelector(focusTarget)
7 | target.focus()
8 | }
9 |
10 | return (
11 |
26 |
27 |
28 | focusTarget('#mainContent')}>
29 | Aller au contenu
30 |
31 |
32 |
33 | focusTarget('#mainNavigation')}
36 | >
37 | Menu
38 |
39 |
40 |
41 |
42 | À propos
43 |
44 |
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/source/sites/publicodes/StoreContext.js:
--------------------------------------------------------------------------------
1 | import { createContext, useReducer } from 'react'
2 | import { reducer, initialState } from './reducers'
3 |
4 | const StoreContext = createContext(initialState)
5 |
6 | const StoreProvider = ({ children }) => {
7 | // Set up reducer with useReducer and our defined reducer, initialState from reducers.js
8 | const [state, dispatch] = useReducer(reducer, initialState)
9 |
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
17 | export { StoreContext, StoreProvider }
18 |
--------------------------------------------------------------------------------
/source/sites/publicodes/SurveyModal.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react'
2 | import styled from 'styled-components'
3 | import Modal from 'Components/Modal'
4 |
5 | // Component used to display survey in Modal.
6 | // First survey in September 2022 : https://github.com/datagir/nosgestesclimat-site/commit/a5d9ea23e0cf432bfc8919b75ff9dc8432c0ced4
7 |
8 | export default function SurveyModal({ showSurveyModal, setShowSurveyModal }) {
9 | const [loading, setLoading] = useState(true)
10 | return (
11 |
16 | {loading && (
17 |
24 | Chargement
25 |
26 | )}
27 |
35 |
36 | }
37 | />
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/source/sites/publicodes/catégories.js:
--------------------------------------------------------------------------------
1 | const catégories = [
2 | 'transport',
3 | 'logement',
4 | 'numérique',
5 | 'vêtements',
6 | 'divers',
7 | 'nourriture',
8 | ]
9 | const catégorie = ({ catégorie, dottedName }) => {
10 | if (catégorie && catégories.includes(catégorie)) return catégorie
11 |
12 | const found = catégories.find((a) => dottedName.includes(a + ' . '))
13 | return found || 'divers'
14 | }
15 |
16 | export default (rules) => {
17 | const raw = Object.entries(
18 | rules.reduce((memo, next) => {
19 | const category = catégorie(next)
20 | memo[category] = [...(memo[category] || []), next]
21 | return memo
22 | }, {})
23 | )
24 | return raw.sort(([c1], [c2]) =>
25 | c1 === 'nourriture' ? 1 : catégories.indexOf(c1) - catégories.indexOf(c2)
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/source/sites/publicodes/chart/DetailedBarChartIcon.tsx:
--------------------------------------------------------------------------------
1 | export default () => (
2 |
15 | {[10, 6, 3].map((score) => (
16 |
22 | ))}
23 |
24 | )
25 |
--------------------------------------------------------------------------------
/source/sites/publicodes/chart/SpecializedVisualisation.tsx:
--------------------------------------------------------------------------------
1 | import RavijenChart from '@/sites/publicodes/chart/RavijenChart'
2 | import { motion } from 'framer-motion'
3 |
4 | export const activatedSpecializedVisualisations = [
5 | 'services sociétaux . question rhétorique',
6 | ]
7 |
8 | export default ({ currentQuestion, categoryColor, value }) => {
9 | if (currentQuestion === 'services sociétaux . question rhétorique') {
10 | return (
11 |
22 |
29 |
30 | )
31 | }
32 |
33 | // Not ready yet. Animation should start from the bottom
34 | // Should be iterated
35 | /*
36 | if (false && currentQuestion === 'logement . habitants') {
37 | return (
38 |
39 |
46 |
47 | )
48 | }
49 | */
50 |
51 | return null
52 | }
53 |
--------------------------------------------------------------------------------
/source/sites/publicodes/chart/TriangleShape.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | const TriangleShape = ({ color }) => (
4 |
10 |
22 |
23 | )
24 |
25 | export default TriangleShape
26 |
--------------------------------------------------------------------------------
/source/sites/publicodes/chart/Value.js:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { humanWeight } from '../HumanWeight'
3 |
4 | export default ({ nodeValue, color, completed, demoMode }) => {
5 | const { t, i18n } = useTranslation()
6 | const [value, unit] = humanWeight({ t, i18n }, nodeValue, true)
7 |
8 | return (
9 |
17 | {value} {unit}
18 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/source/sites/publicodes/chart/useContinuousCategory.tsx:
--------------------------------------------------------------------------------
1 | import { Category, questionCategoryName } from 'Components/publicodesUtils'
2 | import { useEffect, useState } from 'react'
3 | import { useSelector } from 'react-redux'
4 | import { currentQuestionSelector } from 'Selectors/simulationSelectors'
5 |
6 | // This is necessary to bring continuity to an animation
7 | // it avoids returning null values (reseting the simulation) and then the
8 | // same category as before null
9 | export default (categories: Category[]): Category | undefined => {
10 | const [displayedCategory, setDisplayedCategory] = useState(undefined)
11 | const currentQuestion = useSelector(currentQuestionSelector)
12 |
13 | useEffect(() => {
14 | const newCategory =
15 | currentQuestion &&
16 | categories.find(
17 | ({ dottedName }) => dottedName === questionCategoryName(currentQuestion)
18 | )
19 | newCategory && setDisplayedCategory(newCategory)
20 | }, [currentQuestion, categories])
21 |
22 | return displayedCategory
23 | }
24 |
--------------------------------------------------------------------------------
/source/sites/publicodes/chrono.js:
--------------------------------------------------------------------------------
1 | // NOTE: could it be removed?
2 | export let année = 1
3 | export let siècle = année * 100
4 | export let millénaire = année * 1000
5 | export let décennie = année * 10
6 | export let semestre = année / 2
7 | export let mois = année / 12
8 | export let trimestre = mois * 3
9 | export let jour = mois / 30.4367
10 | export let heure = jour / 24
11 | export let minute = heure / 60
12 | export let seconde = minute / 60
13 |
--------------------------------------------------------------------------------
/source/sites/publicodes/chrono.yaml:
--------------------------------------------------------------------------------
1 | - nom: millénaire
2 | formule: 1000 * année
3 | - nom: centenaire
4 | formule: 100 * année
5 | - nom: décade
6 | formule: 10 * année
7 | - nom: année
8 | formule: 1
9 | - nom: semestre
10 | formule: 6 * mois
11 | - nom: trimestre
12 | formule: 3 * mois
13 | - nom: mois
14 | formule: année /12
15 | - nom: jour
16 | formule: mois / 30.43666
17 | - nom: matinée
18 | formule: jour / 4
19 | - nom: heure
20 | formule: jour / 24
21 | - nom: demi-heure
22 | formule: heure / 2
23 | - nom: minute
24 | formule: heure / 60
25 | - nom: seconde
26 | formule: minute / 60
27 |
--------------------------------------------------------------------------------
/source/sites/publicodes/conference/ConferenceBarLazy.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { Trans } from 'react-i18next'
3 | import { useSelector } from 'react-redux'
4 | import { WithEngine } from '../../../RulesProvider'
5 |
6 | const ConferenceBar = React.lazy(
7 | () => import(/* webpackChunkName: 'ConferenceBar' */ './ConferenceBar')
8 | )
9 |
10 | export default () => {
11 | const conference = useSelector((state) => state.conference)
12 | if (!conference) {
13 | return null
14 | }
15 |
16 | return (
17 |
20 | Chargement
21 |
22 | }
23 | >
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/source/sites/publicodes/conference/GroupSwitch.tsx:
--------------------------------------------------------------------------------
1 | import Title from '@/components/groupe/Title'
2 | import AutoCanonicalTag from '@/components/utils/AutoCanonicalTag'
3 | import Meta from '@/components/utils/Meta'
4 | import { ScrollToTop } from '@/components/utils/Scroll'
5 | import { useState } from 'react'
6 | import { useTranslation } from 'react-i18next'
7 | import Instructions from './Instructions'
8 | import { generateRoomName } from './utils'
9 |
10 | export default () => {
11 | const [newRoom, setNewRoom] = useState(generateRoomName())
12 | const { t } = useTranslation()
13 |
14 | return (
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/source/sites/publicodes/conference/NoSurveyCreatedWarning.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { Link } from 'react-router-dom'
3 | import IllustratedMessage from '../../../components/ui/IllustratedMessage'
4 |
5 | export default () => (
6 |
10 |
11 |
12 | Attention, il n'existe aucun sondage à cette adresse. Pour lancer un
13 | sondage, l'organisateur doit d'abord le créer sur la page du{' '}
14 | mode groupe.
15 |
16 |
17 |
18 | 💡{' '}
19 |
20 | Peut-être avez-vous fait une faute de frappe dans l'adresse du
21 | sondage ? Pensez notamment à bien respecter les majuscules, à copier
22 | coller l'adresse exacte ou à utiliser le QR code.
23 |
24 |
25 | >
26 | }
27 | backgroundcolor={'var(--lighterColor)'}
28 | />
29 | )
30 |
--------------------------------------------------------------------------------
/source/sites/publicodes/conference/NoTestMessage.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { Link } from 'react-router-dom'
3 | import IllustratedMessage from '../../../components/ui/IllustratedMessage'
4 |
5 | export default ({ setHasDataState }) => (
6 |
10 |
11 |
12 | Bienvenue dans le mode groupe de Nos Gestes Climat ! Vous n'avez pas
13 | encore débuté votre test, lancez-vous !
14 |
15 |
16 |
27 |
28 |
29 | Faire mon test
30 |
31 |
32 | {
35 | setHasDataState(true)
36 | }}
37 | >
38 | 🧮 Voir les réponses
39 |
40 |
41 |
42 | }
43 | />
44 | )
45 |
--------------------------------------------------------------------------------
/source/sites/publicodes/conference/SurveyBarLazy.tsx:
--------------------------------------------------------------------------------
1 | // TODO: factorizable with ConferenceBarLazy
2 |
3 | import React, { Suspense } from 'react'
4 | import { Trans } from 'react-i18next'
5 | import { useSelector } from 'react-redux'
6 | import { WithEngine } from '../../../RulesProvider'
7 |
8 | const SurveyBar = React.lazy(
9 | () => import(/* webpackChunkName: 'SurveyBar' */ './SurveyBar')
10 | )
11 |
12 | export default () => {
13 | const survey = useSelector((state) => state.survey)
14 | if (!survey) {
15 | return null
16 | }
17 |
18 | return (
19 |
22 | Chargement
23 |
24 | }
25 | >
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/source/sites/publicodes/conference/conferenceStyle.tsx:
--------------------------------------------------------------------------------
1 | export const backgroundConferenceAnimation = `
2 | @keyframes gradient {
3 | 0% {
4 | background-position: 0% 50%;
5 | }
6 | 50% {
7 | background-position: 100% 50%;
8 | }
9 | 100% {
10 | background-position: 0% 50%;
11 | }
12 | }
13 | background: linear-gradient(
14 | 90deg,
15 | white -10%,
16 | var(--color) 10%,
17 | #b71540 90%,
18 | white 110%
19 | );
20 | background-size: 400% 400%;
21 | animation: gradient 15s ease infinite;
22 | `
23 |
--------------------------------------------------------------------------------
/source/sites/publicodes/conference/périodesGéologiques.json:
--------------------------------------------------------------------------------
1 | [
2 | "Quaternaire",
3 | "Pléistocène",
4 | "Holocène",
5 | "Néogène",
6 | "Miocène",
7 | "Pliocène",
8 | "Paléogène",
9 | "Paléocène",
10 | "Éocène",
11 | "Oligocène",
12 | "Crétacé",
13 | "Jurassique",
14 | "Trias",
15 | "Permien",
16 | "Carbonifère",
17 | "Mississippien",
18 | "Pennsylvanien",
19 | "Dévonien",
20 | "Silurien",
21 | "Ordovicien",
22 | "Cambrien",
23 | "Édiacarien",
24 | "Cryogénien",
25 | "Tonien",
26 | "Stenien",
27 | "Ectasien",
28 | "Calymmien",
29 | "Stathérien",
30 | "Orosirien",
31 | "Rhyacien",
32 | "Sidérien"
33 | ]
34 |
--------------------------------------------------------------------------------
/source/sites/publicodes/conference/useDatabase.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { io } from 'socket.io-client'
3 |
4 | const secure = process.env.NODE_ENV === 'development' ? '' : 's'
5 | const protocol = `http${secure}://`
6 | export const serverURL = protocol + process.env.SERVER_URL
7 |
8 | export const answersURL = serverURL + '/answers/'
9 |
10 | export const surveysURL = serverURL + '/surveys/'
11 |
12 | export const simulationURL = serverURL + '/simulation/'
13 |
14 | export const emailSimulationURL = serverURL + '/email-simulation/'
15 |
16 | export const contextURL = serverURL
17 |
18 | export default () => {
19 | const database = useMemo(
20 | () =>
21 | io(`ws${secure}://` + process.env.SERVER_URL).on('error', (err) =>
22 | console.log('ERROR', err)
23 | ),
24 | []
25 | )
26 |
27 | return database
28 | }
29 |
--------------------------------------------------------------------------------
/source/sites/publicodes/enquête/BannerWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { useSelector } from 'react-redux'
3 | import { enquêteSelector } from './enquêteSelector'
4 |
5 | const LazyBanner = React.lazy(
6 | () => import(/* webpackChunkName: 'Banner' */ './Banner')
7 | )
8 |
9 | export default () => {
10 | const enquête = useSelector(enquêteSelector)
11 |
12 | if (!enquête) return null
13 |
14 | return (
15 | Chargement de la bannière d'enquête}>
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/source/sites/publicodes/enquête/ReturnToEnquêteButton.tsx:
--------------------------------------------------------------------------------
1 | import { useTestCompleted } from '@/selectors/simulationSelectors'
2 | import { enquêteSelector } from '@/sites/publicodes/enquête/enquêteSelector'
3 | import { useDispatch, useSelector } from 'react-redux'
4 |
5 | export default ({ simple }) => {
6 | const dispatch = useDispatch()
7 | const enquête = useSelector(enquêteSelector)
8 | const testCompleted = useTestCompleted()
9 |
10 | if (!enquête) {
11 | return null
12 | }
13 |
14 | const id = enquête.userID
15 | const url = 'https://nosgestesclimat.fr'
16 |
17 | return (
18 | dispatch({ type: 'QUIT_ENQUÊTE' })}
26 | >
27 | ✅ Continuer l'enquête
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/source/sites/publicodes/enquête/SendResultButton.tsx:
--------------------------------------------------------------------------------
1 | import { buildEndURL } from '@/components/SessionBar'
2 | import { useEngine } from '@/components/utils/EngineContext'
3 | import { AppState } from '@/reducers/rootReducer'
4 | import { situationSelector } from '@/selectors/simulationSelectors'
5 | import { enquêteSelector } from '@/sites/publicodes/enquête/enquêteSelector'
6 | import { Trans } from 'react-i18next'
7 | import { useSelector } from 'react-redux'
8 |
9 | export default ({}) => {
10 | const { userID } = useSelector(enquêteSelector)
11 | const situation = useSelector(situationSelector),
12 | situationQueryString = Object.entries(situation).reduce(
13 | (memo, [k, v]) =>
14 | memo +
15 | (!memo.length ? '?' : '&') +
16 | encodeURIComponent(k) +
17 | '=' +
18 | encodeURIComponent(typeof v === 'object' ? v.valeur : v),
19 | ''
20 | )
21 | const engine = useEngine()
22 | const rules = useSelector((state: AppState) => state.rules)
23 | const endURL = buildEndURL(rules, engine)
24 | const endQueryString = endURL?.replace('/fin?', '&')
25 |
26 | return (
27 |
31 | Envoyer mes résultats
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/source/sites/publicodes/enquête/enquêteSelector.ts:
--------------------------------------------------------------------------------
1 | import { AppState } from '@/reducers/rootReducer'
2 |
3 | export const enquêteSelector = (state: AppState) => state.enquête
4 |
5 | /* If we decide that the `enquête` is attached to one of the simulations
6 | * we should change it for something like this
7 |
8 | const simulationEnquêteSelector = createSelector(
9 | [currentSimulationSelector],
10 | (simulation) => {
11 | return simulation?.enquête
12 | }
13 | )
14 |
15 | // This would be an intermediate implementation, it is obviously dirty
16 | // See the TODO s in the file persistSimulation.ts
17 | export const enquêteSelector = createSelector(
18 | [stateEnquêteSelector, simulationEnquêteSelector],
19 | (enquête, simulationEnquête) => enquête || simulationEnquête
20 | )
21 | */
22 |
--------------------------------------------------------------------------------
/source/sites/publicodes/enquête/texte.md:
--------------------------------------------------------------------------------
1 | # Enquête
2 |
3 | Bienvenue !
4 |
5 | En arrivant sur le site par ce lien, vous allez faire le test et le parcours Nos Gestes Climat dans un mode adapté pour une enquête.
6 |
7 | 1️⃣ Si vous aviez déjà fait le test dans ce navigateur, vous allez repartir de zéro à l'occasion de cette enquête
8 |
9 | 2️⃣ Vous ferez votre test d'empreinte, qui prendra 5 à 10 minutes, puis vous pourrez découvrir les actions de réduction d'empreinte proposées
10 |
11 | 3️⃣ Pendant votre parcours, vos réponses et les résultats du calcul sont automatiquement sauvegardées de façon anonyme par nosgestesclimat.fr. Elles ne seront utilisée qu'à une fin d'amélioration du parcours utilisateur de Nos Gestes Climat : clarifier les questions du test, proposer des réponses représentatives, etc.
12 |
--------------------------------------------------------------------------------
/source/sites/publicodes/entry.js:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/react'
2 | import 'core-js/stable'
3 | import { createRoot } from 'react-dom/client'
4 | import i18next from '../../locales/i18n'
5 | import { getLangInfos, Lang } from '../../locales/translation'
6 | import App from './App'
7 |
8 | Sentry.init({
9 | dsn: process.env.SENTRY_DSN,
10 | integrations: [new Sentry.BrowserTracing()],
11 | // NOTE(@EmileRoley): Quite an arbitrary value
12 | tracesSampleRate: 0.25,
13 | enabled: process.env.NODE_ENV !== 'development',
14 | environment: process.env.CONTEXT,
15 | })
16 |
17 | Object.keys(Lang).forEach((lang) => {
18 | if (lang !== Lang.Default) {
19 | const infos = getLangInfos(Lang[lang])
20 | console.log(`[i18next] Loading '${infos.abrv}'...`)
21 | i18next.addResourceBundle(infos.abrv, 'translation', infos.uiTrad)
22 | }
23 | })
24 |
25 | let anchor = document.querySelector('#js')
26 |
27 | const root = createRoot(anchor) // createRoot(container!) if you use TypeScript
28 | root.render( )
29 |
--------------------------------------------------------------------------------
/source/sites/publicodes/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/incubateur-ademe/nosgestesclimat-site/4dc6f727d8ec9b25871c8db6b26e1f131747fcae/source/sites/publicodes/logo.png
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/About.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 |
3 | import { Lang } from '../../../locales/translation'
4 | import MarkdownXPage from './MarkdownXPage'
5 |
6 | import contentEn from '../../../locales/pages/en/about.mdx'
7 |
8 | import contentFr from '../../../locales/pages/fr/about.mdx'
9 |
10 | export default () => {
11 | const { t } = useTranslation()
12 | return (
13 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/Accessibility.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { Lang } from '../../../locales/translation'
3 | import MarkdownPage from './MarkdownPage'
4 |
5 | import contentEn from '../../../locales/pages/en/accessibility.md'
6 | // import contentEs from '../../../locales/pages/es/accessibility.md'
7 | import contentFr from '../../../locales/pages/fr/accessibility.md'
8 | // import contentIt from '../../../locales/pages/it/accessibility.md'
9 |
10 | export default () => {
11 | const { t } = useTranslation()
12 | return (
13 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/BlogArticle.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { Lang } from '../../../locales/translation'
3 | import MarkdownPage from './MarkdownPage'
4 |
5 | import { Link, useParams } from 'react-router-dom'
6 | import { blogData } from './BlogData'
7 |
8 | const MarkdownPageWrapper = () => {
9 | const { t } = useTranslation()
10 | const { slug } = useParams()
11 |
12 | const markdownFile = blogData.find((element) => element.slug == slug)
13 | const content = markdownFile?.content || ''
14 | const title = markdownFile?.title
15 | const description = markdownFile?.description
16 |
17 | if (!markdownFile) {
18 | return (
19 |
20 | ← {t('Retour à la liste des articles')}
21 |
22 | {t("Oups, nous n'avons pas d'article correspondant")}
23 |
24 | )
25 | }
26 |
27 | return (
28 |
29 | ← {t('Retour à la liste des articles')}
30 |
35 |
36 | )
37 | }
38 |
39 | export default MarkdownPageWrapper
40 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/CGU.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { Lang } from '../../../locales/translation'
3 | import MarkdownPage from './MarkdownPage'
4 |
5 | import contentEn from '../../../locales/pages/en/CGU.md'
6 | // import contentEs from '../../../locales/pages/es/CGU.md'
7 | import contentFr from '../../../locales/pages/fr/CGU.md'
8 | // import contentIt from '../../../locales/pages/it/CGU.md'
9 |
10 | export default () => {
11 | const { t } = useTranslation()
12 | return (
13 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/Diffuser.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { Lang } from '../../../locales/translation'
3 | import MarkdownPage from './MarkdownPage'
4 |
5 | import contentEn from '../../../locales/pages/en/diffuser.md'
6 | // import contentEs from '../../../locales/pages/es/diffuser.md'
7 | import contentFr from '../../../locales/pages/fr/diffuser.md'
8 | // import contentIt from '../../../locales/pages/it/diffuser.md'
9 |
10 | export default () => {
11 | const { t } = useTranslation()
12 | return (
13 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/DocumentationStyle.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export default styled.div`
4 | padding: 0 0.6rem;
5 | margin-bottom: 1rem;
6 | #documentation-rule-root > p:first-of-type {
7 | display: inline-block;
8 | background: var(--lighterColor);
9 | padding: 0.4rem 0.6rem 0.2rem;
10 | }
11 | header {
12 | color: var(--textColor);
13 | small {
14 | color: inherit;
15 | }
16 | a {
17 | color: var(--textColor);
18 | }
19 | a:hover {
20 | background: var(--darkerColor) !important;
21 | color: white !important;
22 | }
23 | h1 {
24 | color: inherit;
25 | margin-top: 0.6rem;
26 | margin-bottom: 0.6rem;
27 | a {
28 | text-decoration: none;
29 | }
30 | }
31 | background: linear-gradient(60deg, var(--darkColor) 0%, var(--color) 100%);
32 | padding: 0.6rem 1rem;
33 | box-shadow: 0 1px 3px rgba(var(--rgbColor), 0.12),
34 | 0 1px 2px rgba(var(--rgbColor), 0.24);
35 | border-radius: 0.4rem;
36 | }
37 | button {
38 | color: inherit;
39 | }
40 | span {
41 | background: inherit;
42 | }
43 | small {
44 | background: none !important;
45 | }
46 | li {
47 | &.active .content {
48 | background-color: transparent !important;
49 | a:hover {
50 | color: white !important;
51 | }
52 | }
53 | }
54 | #documentation-rule-root > article {
55 | max-width: 800px;
56 | }
57 | `
58 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/MarkdownPage.tsx:
--------------------------------------------------------------------------------
1 | import { Markdown } from '@/components/utils/markdown'
2 | import { useTranslation } from 'react-i18next'
3 |
4 | import AutoCanonicalTag from '@/components/utils/AutoCanonicalTag'
5 | import Meta from '@/components/utils/Meta'
6 | import { ScrollToTop } from '@/components/utils/Scroll'
7 | import { getMarkdownInCurrentLang, Lang } from '@/locales/translation'
8 |
9 | export type PageProps = {
10 | markdownFiles: Array<[Lang, string]>
11 | // Information about the page metadata
12 | title?: string
13 | description?: string
14 | image?: string
15 | }
16 |
17 | export default ({ markdownFiles, title, description, image }: PageProps) => {
18 | const { i18n } = useTranslation()
19 | const lang: Lang = i18n.language as Lang
20 |
21 | const content = getMarkdownInCurrentLang(markdownFiles, lang)
22 |
23 | return (
24 |
25 | {title && description && (
26 |
27 | )}
28 |
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/MarkdownXPage.tsx:
--------------------------------------------------------------------------------
1 | import type { MDXContent } from 'mdx/types'
2 | import { useTranslation } from 'react-i18next'
3 |
4 | import Meta from '@/components/utils/Meta'
5 | import { ScrollToTop } from '@/components/utils/Scroll'
6 | import { getMarkdownXInCurrentLang, Lang } from '@/locales/translation'
7 |
8 | export type PageProps = {
9 | markdownFiles: Array<[Lang, MDXContent]>
10 | // Information about the page metadata
11 | title?: string
12 | description?: string
13 | image?: string
14 | }
15 |
16 | export default ({ markdownFiles, title, description, image }: PageProps) => {
17 | const { i18n } = useTranslation()
18 | const lang: Lang = i18n.language as Lang
19 |
20 | const Content = getMarkdownXInCurrentLang(markdownFiles, lang)
21 |
22 | return (
23 |
24 | {title && description && (
25 |
26 | )}
27 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/Méthode.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { Lang } from '../../../locales/translation'
3 | import MarkdownPage from './MarkdownPage'
4 |
5 | import contentEn from '../../../locales/pages/en/méthode.md'
6 | // import contentEs from '../../../locales/pages/es/méthode.md'
7 | import contentFr from '../../../locales/pages/fr/méthode.md'
8 | // import contentIt from '../../../locales/pages/it/méthode.md'
9 |
10 | export default () => {
11 | const { t } = useTranslation()
12 | return (
13 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/PetrogazLanding.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { Lang } from '../../../locales/translation'
3 | import MarkdownPage from './MarkdownPage'
4 |
5 | import contentEn from '../../../locales/pages/en/petrogazLanding.md'
6 | import contentFr from '../../../locales/pages/fr/petrogazLanding.md'
7 |
8 | export default () => {
9 | const { t } = useTranslation()
10 | return (
11 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/Privacy.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 | import { Lang } from '../../../locales/translation'
3 | import MarkdownPage from './MarkdownPage'
4 |
5 | import contentEn from '../../../locales/pages/en/privacy.md'
6 | // import contentEs from '../../../locales/pages/es/privacy.md'
7 | import contentFr from '../../../locales/pages/fr/privacy.md'
8 | // import contentIt from '../../../locales/pages/it/privacy.md'
9 |
10 | export default () => {
11 | const { t } = useTranslation()
12 |
13 | return (
14 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/Stats.js:
--------------------------------------------------------------------------------
1 | import { QueryClient, QueryClientProvider } from 'react-query'
2 |
3 | import StatsContent from '@/components/stats/StatsContent'
4 | import AutoCanonicalTag from '@/components/utils/AutoCanonicalTag'
5 | import { ScrollToTop } from '@/components/utils/Scroll'
6 |
7 | const queryClient = new QueryClient({
8 | defaultOptions: {
9 | queries: {
10 | staleTime: Infinity,
11 | refetchOnWindowFocus: false,
12 | refetchInterval: false,
13 | },
14 | },
15 | })
16 |
17 | export default function Dashboard() {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/source/sites/publicodes/pages/editorialisedModels.yaml:
--------------------------------------------------------------------------------
1 | - bilan
2 | - services sociétaux
3 | - alimentation . plats
4 | - logement . chauffage . empreinte par défaut
5 | - transport . voiture . empreinte
6 | - alimentation . déchets
7 |
--------------------------------------------------------------------------------
/source/sites/publicodes/personas/Summary.tsx:
--------------------------------------------------------------------------------
1 | import { Markdown } from '@/components/utils/markdown'
2 | import { Persona } from '@/sites/publicodes/personas/personasUtils'
3 | import { Trans } from 'react-i18next'
4 |
5 | export default ({ persona }: { persona: Persona }) => {
6 | return persona.description !== undefined ? (
7 |
8 |
9 | Description
10 | {':'}
11 |
12 | {persona.description}
13 |
14 | ) : null
15 | }
16 |
--------------------------------------------------------------------------------
/source/sites/publicodes/questionConfig.js:
--------------------------------------------------------------------------------
1 | export const questionConfig = {
2 | 'non prioritaires': ['divers . autres produits'],
3 | 'liste prioritaire': [
4 | 'alimentation . boisson . chaude',
5 | 'divers . animaux domestiques',
6 | ],
7 | 'liste blanche': [],
8 | 'liste noire': ['transport . voiture . aide km'],
9 | }
10 |
--------------------------------------------------------------------------------
/source/sites/publicodes/reducers.js:
--------------------------------------------------------------------------------
1 | let initialState = {
2 | scenario: 'B'
3 | }
4 |
5 | let reducer = (state = initialState, action) => {
6 | switch (action.type) {
7 | case 'SET_SCENARIO':
8 | return {
9 | ...state,
10 | scenario: action.scenario
11 | }
12 | default:
13 | throw new Error('Unexpected action')
14 | }
15 | }
16 | export { initialState, reducer }
17 |
--------------------------------------------------------------------------------
/source/sites/publicodes/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------
/source/sites/publicodes/sitePaths.js:
--------------------------------------------------------------------------------
1 | // This is a placeholder, not currently used.
2 | // see betagouv/mon-entreprise to manage sitePaths for i18n
3 | const sitePath = {
4 | index: '',
5 | documentation: {
6 | index: '/documentation',
7 | exemples: '/exemples',
8 | },
9 | contact: '/contact',
10 | }
11 |
12 | export default () => sitePath
13 |
--------------------------------------------------------------------------------
/source/sites/publicodes/tutorial/Categories.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { WithEngine } from '../../../RulesProvider'
3 | import Chart from '../chart'
4 |
5 | export default () => (
6 | <>
7 |
8 | D'où vient notre empreinte ?
9 |
10 | Prendre la voiture, manger un steak, chauffer sa maison, se faire
11 | soigner, acheter une TV...
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 | L'empreinte de notre consommation individuelle, c'est la somme de toutes
24 | ces activités qui font notre vie moderne.{' '}
25 |
26 |
27 | >
28 | )
29 |
--------------------------------------------------------------------------------
/source/sites/publicodes/tutorial/ClimateWarming.tsx:
--------------------------------------------------------------------------------
1 | import GreenhouseEffect from 'Images/greenhouse-effect.svg'
2 | import { Trans } from 'react-i18next'
3 |
4 | export default () => (
5 |
6 |
7 | Mon empreinte climat 😶🌫️ ?
8 |
9 |
10 | Pas de panique, on vous explique ce que c'est.
11 |
12 | La planète se réchauffe dangereusement , au fur et à
13 | mesure des gaz à effet de serre que l'on émet.
14 |
15 |
16 |
17 |
18 |
19 | Ce test vous donne en ⏱️ 10 minutes chrono{' '}
20 | une mesure de votre part dans ce réchauffement.
21 |
22 |
23 |
24 | )
25 |
--------------------------------------------------------------------------------
/source/sites/publicodes/tutorial/Instructions.tsx:
--------------------------------------------------------------------------------
1 | import { Trans } from 'react-i18next'
2 | import { Link } from 'react-router-dom'
3 |
4 | export default () => (
5 | <>
6 |
7 | Alors, c'est parti ?
8 | Quelques astuces pour vous aider à compléter le test :
9 |
10 | 👤 Répondez aux questions en votre nom, pas au nom de votre foyer :
11 | c'est un test individuel.
12 |
13 |
14 | 💼 Répondez pour votre vie perso, pas pour votre boulot ou études.{' '}
15 | Une seule exception : votre trajet domicile-travail doit être
16 | inclus dans les km parcourus.
17 |
18 |
19 | ❓️ D'autres questions ? Consultez notre{' '}
20 | FAQ à tout moment.
21 |
22 |
23 | >
24 | )
25 |
--------------------------------------------------------------------------------
/source/sites/publicodes/useActions.tsx:
--------------------------------------------------------------------------------
1 | import { AppState } from '@/reducers/rootReducer'
2 | import { useSelector } from 'react-redux'
3 | import { correctValue } from '../../components/publicodesUtils'
4 | import { sortBy } from '../../utils'
5 | import { disabledAction, supersededAction } from './ActionVignette'
6 |
7 | export default ({ focusedAction, rules, radical, engine, metric }) => {
8 | const flatActions = metric ? rules[`actions ${metric}`] : rules['actions']
9 | const objectifs = ['bilan', ...flatActions.formule.somme]
10 | const actionChoices = useSelector((state: AppState) => state.actionChoices)
11 | const targets = objectifs.map((o) => engine.evaluate(o))
12 | const actions = targets.filter((t) => t.dottedName !== 'bilan')
13 | const sortedActionsByImpact = sortBy(
14 | (a) => (radical ? 1 : -1) * correctValue(a)
15 | )(actions)
16 | const interestingActions = sortedActionsByImpact.filter((action) => {
17 | const flatRule = rules[action.dottedName]
18 | const superseded = supersededAction(action.dottedName, rules, actionChoices)
19 | const disabled = disabledAction(flatRule, action.nodeValue)
20 | return !superseded && (action.dottedName === focusedAction || !disabled)
21 | })
22 | return { interestingActions, targets, rawActionsList: actions }
23 | }
24 |
--------------------------------------------------------------------------------
/source/sites/publicodes/utils.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DottedName,
3 | encodeRuleNameToSearchParam,
4 | } from '@/components/publicodesUtils'
5 |
6 | export function isFluidLayout(encodedPathname: string): boolean {
7 | const pathname = decodeURIComponent(encodedPathname)
8 |
9 | return (
10 | pathname === '/' ||
11 | pathname.startsWith('/nouveautés') ||
12 | pathname.startsWith('/documentation') ||
13 | pathname.startsWith('/international')
14 | )
15 | }
16 |
17 | export function getFocusedCategoryURLSearchParams(
18 | focusedCategory: string | null
19 | ): URLSearchParams {
20 | if (focusedCategory === null) {
21 | return new URLSearchParams()
22 | }
23 | return new URLSearchParams({ catégorie: focusedCategory })
24 | }
25 |
26 | export function getQuestionURLSearchParams(
27 | ruleName: DottedName
28 | ): URLSearchParams {
29 | const encodedRuleName = encodeRuleNameToSearchParam(ruleName)
30 |
31 | if (encodedRuleName === undefined) {
32 | return new URLSearchParams()
33 | }
34 |
35 | return new URLSearchParams({ question: encodedRuleName })
36 | }
37 |
--------------------------------------------------------------------------------
/source/storage/persistEverything.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'Actions/actions'
2 | import { AppState } from 'Reducers/rootReducer'
3 | import { Store } from 'redux'
4 | import { debounce, omit } from '../utils'
5 | import safeLocalStorage from './safeLocalStorage'
6 |
7 | const VERSION = 1
8 |
9 | const LOCAL_STORAGE_KEY = 'ecolab-climat::global-state:v' + VERSION
10 |
11 | type OptionsType = {
12 | except?: Array
13 | }
14 | export const persistEverything =
15 | (options: OptionsType = {}) =>
16 | (store: Store): void => {
17 | const listener = () => {
18 | const state = store.getState()
19 | safeLocalStorage.setItem(
20 | LOCAL_STORAGE_KEY,
21 | JSON.stringify(omit(options.except || [], state))
22 | )
23 | }
24 | store.subscribe(debounce(1000, listener))
25 | }
26 |
27 | export function retrievePersistedState(): AppState {
28 | const serializedState = safeLocalStorage.getItem(LOCAL_STORAGE_KEY)
29 | return serializedState ? JSON.parse(serializedState) : null
30 | }
31 |
--------------------------------------------------------------------------------
/source/storage/safeLocalStorage.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | removeItem: function(key: string) {
3 | try {
4 | return window.localStorage.removeItem(key)
5 | } catch (error) {
6 | if (error.name === 'SecurityError') {
7 | // eslint-disable-next-line no-console
8 | console.warn(
9 | '[localStorage] Unable to remove item due to security settings'
10 | )
11 | }
12 | return null
13 | }
14 | },
15 | getItem: function(key: string) {
16 | try {
17 | return window.localStorage.getItem(key)
18 | } catch (error) {
19 | if (error.name === 'SecurityError') {
20 | // eslint-disable-next-line no-console
21 | console.warn(
22 | '[localStorage] Unable to get item due to security settings'
23 | )
24 | }
25 | return null
26 | }
27 | },
28 | setItem: function(key: string, value: string) {
29 | try {
30 | return window.localStorage.setItem(key, value)
31 | } catch (error) {
32 | if (error.name === 'SecurityError') {
33 | // eslint-disable-next-line no-console
34 | console.warn(
35 | '[localStorage] Unable to set item due to security settings'
36 | )
37 | }
38 | return null
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/source/types/.gitignore:
--------------------------------------------------------------------------------
1 | dottednames.json
2 |
--------------------------------------------------------------------------------
/source/types/app-env.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS {
2 | interface ProcessEnv {
3 | EN_SITE: string
4 | FR_SITE: string
5 | NODE_ENV: 'development' | 'production' | 'test'
6 | ANALYZE_BUNDLE: '0' | '1'
7 |
8 | // Github actions env variables
9 | // https://docs.github.com/en/free-pro-team@latest/actions/reference/environment-variables
10 | GITHUB_REF: string
11 | GITHUB_SHA: string
12 |
13 | // .env variables
14 | GITHUB_API_SECRET: string
15 | DEEPL_API_SECRET: string
16 | INSEE_SIRENE_API_SECRET: string
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/source/types/css-prop.d.ts:
--------------------------------------------------------------------------------
1 | import 'styled-components/cssprop'
2 |
--------------------------------------------------------------------------------
/source/types/groups.ts:
--------------------------------------------------------------------------------
1 | import { Simulation } from './simulation'
2 |
3 | export type Member = {
4 | _id: string
5 | name: string
6 | email?: string
7 | simulation: Simulation
8 | userId: string
9 | results: SimulationResults
10 | }
11 |
12 | export type Group = {
13 | _id: string
14 | name: string
15 | emoji: string
16 | members: Member[]
17 | owner: {
18 | _id: string
19 | name: string
20 | email?: string
21 | userId: string
22 | }
23 | }
24 |
25 | export type SimulationResults = {
26 | total: string
27 | transport: {
28 | value: string
29 | variation: string
30 | }
31 | transports: {
32 | value: string
33 | variation: string
34 | }
35 | alimentation: {
36 | value: string
37 | variation: string
38 | }
39 | logement: {
40 | value: string
41 | variation: string
42 | }
43 | divers: {
44 | value: string
45 | variation: string
46 | }
47 | 'services sociétaux': {
48 | value: string
49 | variation: string
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/source/types/iframe-resizer.d.ts:
--------------------------------------------------------------------------------
1 | interface Window {
2 | parentIFrame?: any
3 | }
4 |
--------------------------------------------------------------------------------
/source/types/rating.ts:
--------------------------------------------------------------------------------
1 | export type Rating = 0 | 1 | 2 | 3 | 'no_display' | 'display' | 'refuse'
2 |
--------------------------------------------------------------------------------
/source/types/simulation.ts:
--------------------------------------------------------------------------------
1 | import { DottedName } from '@/components/publicodesUtils'
2 | import { Persona } from '@/sites/publicodes/personas/personasUtils'
3 |
4 | export type Situation = Record
5 |
6 | type QuestionsKind =
7 | | "à l'affiche"
8 | | 'non prioritaires'
9 | | 'liste'
10 | | 'liste noire'
11 |
12 | export type ObjectifsConfig =
13 | | Array
14 | | Array<{ icône: string; nom: string; objectifs: Array }>
15 |
16 | export type SimulationConfig = {
17 | objectifs: ObjectifsConfig
18 | 'objectifs cachés': Array
19 | situation: Simulation['situation']
20 | bloquant?: Array
21 | questions?: Partial>>
22 | branches?: Array<{ nom: string; situation: SimulationConfig['situation'] }>
23 | 'unité par défaut': string
24 | }
25 |
26 | export type StoredTrajets = Record
27 |
28 | export type Simulation = {
29 | config: SimulationConfig
30 | url: string
31 | hiddenNotifications: Array
32 | situation: Situation
33 | hiddenControls?: Array
34 | targetUnit?: string
35 | foldedSteps?: Array
36 | unfoldedStep?: DottedName | null
37 | persona?: Persona
38 | date?: Date
39 | id?: string
40 | eventsSent?: Record
41 | actionChoices?: Record
42 | storedTrajets?: StoredTrajets
43 | }
44 |
--------------------------------------------------------------------------------
/source/types/values.ts:
--------------------------------------------------------------------------------
1 | export type ButtonSize = 'sm' | 'md' | 'lg'
2 |
--------------------------------------------------------------------------------
/source/types/worker-loader.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'worker-loader*' {
2 | class WebpackWorker extends Worker {
3 | constructor()
4 | }
5 |
6 | export = WebpackWorker
7 | }
8 |
--------------------------------------------------------------------------------
/source/utils/fetchAddUserToGroup.ts:
--------------------------------------------------------------------------------
1 | import { GROUP_URL } from '@/constants/urls'
2 | import { Group, SimulationResults } from '@/types/groups'
3 | import { Simulation } from '@/types/simulation'
4 |
5 | type Props = {
6 | group: Group
7 | name: string
8 | email?: string
9 | userId: string
10 | simulation?: Simulation
11 | results?: SimulationResults
12 | }
13 |
14 | export const fetchAddUserToGroup = async ({
15 | group,
16 | name,
17 | email,
18 | userId,
19 | simulation,
20 | results,
21 | }: Props) => {
22 | const response = await fetch(`${GROUP_URL}/add-member`, {
23 | method: 'POST',
24 | body: JSON.stringify({
25 | _id: group._id,
26 | member: {
27 | name,
28 | email: email || '',
29 | userId,
30 | simulation,
31 | results,
32 | },
33 | }),
34 | headers: {
35 | Accept: 'application/json',
36 | 'Content-Type': 'application/json',
37 | },
38 | })
39 |
40 | if (!response.ok) {
41 | throw new Error('Error while updating group')
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/source/utils/fetchGroup.ts:
--------------------------------------------------------------------------------
1 | import { GROUP_URL } from '@/constants/urls'
2 | import { Group } from '@/types/groups'
3 |
4 | export const fetchGroup = async (groupId: string) => {
5 | const response = await fetch(`${GROUP_URL}/${groupId}`)
6 |
7 | if (!response.ok) {
8 | throw new Error('Error while fetching group')
9 | }
10 |
11 | const group: Group = await response.json()
12 |
13 | return group
14 | }
15 |
--------------------------------------------------------------------------------
/source/utils/fetchUpdateGroupMember.ts:
--------------------------------------------------------------------------------
1 | import { GROUP_URL } from '@/constants/urls'
2 | import { SavedSimulation } from '@/selectors/storageSelectors'
3 | import { Group, SimulationResults } from '@/types/groups'
4 |
5 | type Props = {
6 | group: Group
7 | userId: string
8 | simulation: SavedSimulation
9 | results: SimulationResults | undefined
10 | }
11 |
12 | export const fetchUpdateGroupMember = async ({
13 | group,
14 | userId,
15 | simulation,
16 | results,
17 | }: Props) => {
18 | const response = await fetch(`${GROUP_URL}/update-member`, {
19 | method: 'POST',
20 | body: JSON.stringify({
21 | _id: group._id,
22 | memberUpdates: {
23 | userId,
24 | simulation,
25 | results,
26 | },
27 | }),
28 | headers: {
29 | Accept: 'application/json',
30 | 'Content-Type': 'application/json',
31 | },
32 | })
33 |
34 | if (!response.ok) {
35 | throw new Error('Error while updating group')
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/source/utils/getIsSimulationValid.ts:
--------------------------------------------------------------------------------
1 | import { Simulation } from '@/types/simulation'
2 |
3 | export const getIsSimulationValid = (simulation: Simulation): boolean => {
4 | return !!simulation?.id
5 | }
6 |
--------------------------------------------------------------------------------
/source/utils/getSimulationResults.ts:
--------------------------------------------------------------------------------
1 | import { correctValue, extractCategories } from '@/components/publicodesUtils'
2 | import { SimulationResults } from '@/types/groups'
3 | import Engine from 'publicodes'
4 |
5 | export const getSimulationResults = ({
6 | engine,
7 | }: {
8 | engine: Engine | undefined
9 | }): SimulationResults | undefined => {
10 | if (!engine) {
11 | return undefined
12 | }
13 |
14 | const resultsObject = {} as SimulationResults
15 | const rules = engine.getParsedRules()
16 | const categories = extractCategories(rules, engine)
17 |
18 | categories.forEach((category) => {
19 | resultsObject[category.name] = (
20 | Math.round(((category.nodeValue as number) ?? 0) / 10) / 100
21 | ).toFixed(2)
22 | })
23 |
24 | const { nodeValue: rawNodeValue, unit } = engine.evaluate('bilan')
25 | const valueTotal = correctValue({ nodeValue: rawNodeValue, unit })
26 |
27 | resultsObject.total = ((valueTotal as number) / 10 / 100).toFixed(2)
28 |
29 | return resultsObject
30 | }
31 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./source/**/*.{tsx,js}'],
4 | theme: {
5 | extend: {
6 | colors: {
7 | // primary: '#491273',
8 | primary: '#5758BB',
9 | // primaryLight: '#E8DFEE',
10 | primaryLight: '#e6e6f5',
11 | primaryDark: '#32337B',
12 | primaryBorder: 'rgba(73, 18, 115, 0.1)',
13 | title: '#32337B',
14 | secondary: '#3496E0',
15 | lightGrey: '#F8F8F7',
16 | grey: {
17 | 100: '#F8F8F7',
18 | 200: '#E3E3DB',
19 | },
20 | pink: {
21 | 100: '#FAF0FA',
22 | 200: '#FEDEF1',
23 | 500: '#D40983',
24 | },
25 | },
26 | },
27 | },
28 | plugins: [],
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "module": "esnext",
5 | "target": "esnext",
6 | "jsx": "react-jsx",
7 | "allowJs": true,
8 | "allowSyntheticDefaultImports": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "resolveJsonModule": true,
11 | "noImplicitThis": true,
12 | "strictBindCallApply": true,
13 | "strictFunctionTypes": true,
14 | "strictNullChecks": true,
15 | "strictPropertyInitialization": true,
16 | "skipLibCheck": true,
17 | "baseUrl": "source",
18 | "sourceMap": true,
19 | "paths": {
20 | "@/*": ["*"],
21 | "Actions/*": ["actions/*"],
22 | "Components/*": ["components/*"],
23 | "Pages/*": ["sites/publicodes/pages/*"],
24 | "Enquête/*": ["sites/publicodes/enquête/*"],
25 | "Selectors/*": ["selectors/*"],
26 | "Reducers/*": ["reducers/*"],
27 | "Types/*": ["types/*"],
28 | "Images/*": ["images/*"]
29 | },
30 | "outDir": "build"
31 | },
32 | "include": ["source/**/*", "netlify/**/*"],
33 | "exclude": ["node_modules", "build/**/*"],
34 | "ts-node": {
35 | "compilerOptions": {
36 | "module": "commonjs"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------