Ajoute le nouveau module de feedback (#2433)

* feat: Crée composant FeedbackButton

* feat: Ajoute hook clickoutside + mécanique fermeture auto

* feat: Clean et corrige espaces

* feat: Ajoute box-shadow

* chore: Retire les autres formulaires feedbacks

* fix: Taille modal

* feat: Modifie useFeedback

* feat: Améliore message merci

* feat: Ajoute le mode not satisfied

* feat: Rajoute la logique askFeedback + améliore merci

* fix: Import Emoji

* feat: Ajoute version mobile

* feat: Utilise absoluteSitePaths

* feat: Reduce size of button

* feat: move usefeedback and add embedded mode

* feat: Ajoute trads

* feat: Modifie label

* fix: Avoid passing attrib

* feat: Ajoute bouton fermer sur le module
pull/2444/head
Benjamin Arias 2023-01-05 16:58:36 +01:00 committed by GitHub
parent 0fd1115aad
commit 5584fd1242
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 589 additions and 316 deletions

View File

@ -1,7 +1,7 @@
import { ErrorBoundary } from '@sentry/react'
import { FallbackRender } from '@sentry/react/types/errorboundary'
import rules from 'modele-social'
import { ComponentProps, StrictMode, useMemo } from 'react'
import { ComponentProps, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Route, Routes } from 'react-router-dom'
import styled, { css } from 'styled-components'
@ -114,6 +114,7 @@ const App = () => {
return (
<StyledLayout isEmbedded={isEmbedded}>
{!isEmbedded && <Header />}
<main role="main" id="main">
<a href={`${fullURL}#footer`} className="skip-link print-hidden">
{t('Passer le contenu')}

View File

@ -1,11 +1,15 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components'
import { ScrollToElement } from '@/components/utils/Scroll'
import { TextAreaField, TextField } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
import { Spacing } from '@/design-system/layout'
import { Strong } from '@/design-system/typography'
import { H1 } from '@/design-system/typography/heading'
import { Body } from '@/design-system/typography/paragraphs'
type SubmitError = {
@ -15,7 +19,42 @@ type SubmitError = {
const SHORT_MAX_LENGTH = 254
export default function FeedbackForm() {
const FeedbackThankYouContent = () => {
const { t } = useTranslation()
return (
<>
<StyledEmojiContainer role="img" aria-hidden>
<span>
<Emoji emoji="🙌" />
</span>
</StyledEmojiContainer>
<H1>
<Trans>Merci pour votre message !</Trans>
</H1>
<Body>
<Strong>
<Trans>Notre équipe prend en charge votre retour.</Trans>
</Strong>
</Body>
<Body>
<Trans>
Nous avons à cœur d'améliorer en continu notre site,vos remarques nous
sont donc très précieuses.
</Trans>
</Body>
<Spacing lg />
</>
)
}
export default function FeedbackForm({
isNotSatisfied,
title,
}: {
isNotSatisfied: boolean
title: string
}) {
const [isSubmittedSuccessfully, setIsSubmittedSuccessfully] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [submitError, setSubmitError] = useState<SubmitError | undefined>(
@ -70,73 +109,94 @@ export default function FeedbackForm() {
return (
<ScrollToElement onlyIfNotVisible>
{isSubmittedSuccessfully && (
<StyledBody>Merci de votre retour !</StyledBody>
)}
{isSubmittedSuccessfully && <FeedbackThankYouContent />}
{!isSubmittedSuccessfully && (
<StyledFeedback>
<form
onSubmit={(e) => {
e.preventDefault()
const message = (
document.getElementById('message') as HTMLTextAreaElement
)?.value
const email = (
document.getElementById('email') as HTMLInputElement
)?.value
<>
<H1>{title}</H1>
// message et email sont requis
const isMessageEmpty = !message || message === ''
const isEmailEmpty = !email || email === ''
<StyledFeedback>
<form
onSubmit={(e) => {
e.preventDefault()
const message = (
document.getElementById('message') as HTMLTextAreaElement
)?.value
const email = (
document.getElementById('email') as HTMLInputElement
)?.value
if (isMessageEmpty || isEmailEmpty) {
setSubmitError({
message: isMessageEmpty ? requiredErrorMessage : '',
email: isEmailEmpty ? requiredErrorEmail : '',
})
// message et email sont requis
const isMessageEmpty = !message || message === ''
const isEmailEmpty = !email || email === ''
return
}
if (isMessageEmpty || isEmailEmpty) {
setSubmitError({
message: isMessageEmpty ? requiredErrorMessage : '',
email: isEmailEmpty ? requiredErrorEmail : '',
})
void sendMessage({ message, email })
}}
>
<Body>
Que pouvons-nous améliorer pour mieux répondre à vos attentes ?
</Body>
<StyledTextArea
name="message"
label={t('Votre message (requis)')}
onChange={resetSubmitErrorField('message')}
description={t(
'Éviter de communiquer des informations personnelles'
return
}
void sendMessage({ message, email })
}}
>
{isNotSatisfied && (
<>
<Body>
<Trans>
Vous navez pas été satisfait(e) de votre expérience, nous
en sommes désolé(e)s.
</Trans>
</Body>
</>
)}
id="message"
rows={7}
isDisabled={isLoading}
errorMessage={submitError?.message}
/>
<StyledDiv>
<StyledTextField
id="email"
name="email"
type="email"
label={t('Votre adresse e-mail (requise)')}
<Body>
<Strong>
<Trans>
Que pouvons-nous améliorer pour mieux répondre à vos
attentes ?
</Trans>
</Strong>
</Body>
<StyledTextArea
name="message"
label={t('Votre message (requis)')}
onChange={resetSubmitErrorField('message')}
description={t(
'Renseigner une adresse e-mail pour recevoir une réponse'
'Éviter de communiquer des informations personnelles'
)}
id="message"
rows={7}
isDisabled={isLoading}
maxLength={SHORT_MAX_LENGTH}
autoComplete="email"
errorMessage={submitError?.email}
onChange={resetSubmitErrorField('email')}
errorMessage={submitError?.message}
placeholder={t(
'Ex : Des informations plus claires, un calcul détaillé...'
)}
/>
</StyledDiv>
<StyledButton isDisabled={isLoading} type="submit">
{t('Envoyer')}
</StyledButton>
</form>
</StyledFeedback>
<StyledDiv>
<StyledTextField
id="email"
name="email"
type="email"
label={t('Votre adresse e-mail (requise)')}
description={t(
'Renseigner une adresse e-mail pour recevoir une réponse'
)}
isDisabled={isLoading}
maxLength={SHORT_MAX_LENGTH}
autoComplete="email"
errorMessage={submitError?.email}
onChange={resetSubmitErrorField('email')}
/>
</StyledDiv>
<StyledButton isDisabled={isLoading} type="submit">
{t('Envoyer')}
</StyledButton>
</form>
</StyledFeedback>
</>
)}
</ScrollToElement>
)
@ -174,13 +234,25 @@ const StyledButton = styled(Button)`
margin-top: 1rem;
`
const StyledBody = styled(Body)`
font-size: 1.25rem;
font-family: ${({ theme }) => theme.fonts.main};
text-align: center;
padding: 1rem 0;
`
const StyledDiv = styled.div`
margin-top: 1rem;
`
const StyledEmojiContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-top: 3rem;
& > span {
background-color: ${({ theme }) => theme.colors.extended.grey[200]};
border-radius: 100%;
width: 7.5rem;
padding: 2rem;
font-size: 3rem;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
`

View File

@ -0,0 +1,73 @@
import styled from 'styled-components'
import { Emoji } from '@/design-system/emoji'
export type Feedback = 'mauvais' | 'moyen' | 'bien' | 'très bien'
const FeedbackRating = ({
submitFeedback,
}: {
submitFeedback: (feedbackValue: Feedback) => void
}) => {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
}}
role="list"
>
<div role="listitem">
<EmojiButton onClick={() => submitFeedback('mauvais')}>
<Emoji
emoji="🙁"
aria-label="Pas satisfait, envoyer cette réponse"
aria-hidden={false}
/>
</EmojiButton>
</div>
<div role="listitem">
<EmojiButton onClick={() => submitFeedback('moyen')}>
<Emoji
emoji="😐"
aria-label="Moyennement satisfait, envoyer cette réponse"
aria-hidden={false}
/>
</EmojiButton>
</div>
<div role="listitem">
<EmojiButton onClick={() => submitFeedback('bien')}>
<Emoji
emoji="🙂"
aria-label="Plutôt satisfait, envoyer cette réponse"
aria-hidden={false}
/>
</EmojiButton>
</div>
<div role="listitem">
<EmojiButton onClick={() => submitFeedback('très bien')}>
<Emoji
emoji="😀"
aria-label="Très satisfait, envoyer cette réponse"
aria-hidden={false}
/>
</EmojiButton>
</div>
</div>
)
}
const EmojiButton = styled.button`
font-size: 1.5rem;
padding: 0.6rem;
border: none;
background: none;
transition: transform 0.05s;
will-change: transform;
:hover {
transform: scale(1.3);
}
`
export default FeedbackRating

View File

@ -1,28 +1,27 @@
import React, { useCallback, useContext, useState } from 'react'
import { Trans } from 'react-i18next'
import { useCallback, useContext, useRef, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components'
import { TrackingContext } from '@/ATInternetTracking'
import { Popover } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
import { Grid, Spacing } from '@/design-system/layout'
import Popover from '@/design-system/popover/Popover'
import { FocusStyle } from '@/design-system/global-style'
import { Spacing } from '@/design-system/layout'
import { Strong } from '@/design-system/typography'
import { Link } from '@/design-system/typography/link'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import { H4 } from '@/design-system/typography/heading'
import { StyledLink } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { useOnClickOutside } from '@/hooks/useClickOutside'
import { CurrentSimulatorDataContext } from '@/pages/Simulateurs/metadata'
import * as safeLocalStorage from '../../storage/safeLocalStorage'
import { JeDonneMonAvis } from '../JeDonneMonAvis'
import { INSCRIPTION_LINK } from '../layout/Footer/InscriptionBetaTesteur'
import Form from './FeedbackForm'
type PageFeedbackProps = {
blacklist?: Array<string>
customMessage?: React.ReactNode
customEventName?: string
}
import { useFeedback } from '../layout/Footer/useFeedback'
import FeedbackForm from './FeedbackForm'
import FeedbackRating, { Feedback } from './FeedbackRating'
const localStorageKey = (url: string) => `app::feedback::v3::${url}`
const setFeedbackGivenForUrl = (url: string) => {
@ -33,7 +32,7 @@ const setFeedbackGivenForUrl = (url: string) => {
}
// Ask for feedback again after 4 months
const askFeedback = (url: string) => {
const getShouldAskFeedback = (url: string) => {
const previousFeedbackDate = safeLocalStorage.getItem(localStorageKey(url))
if (!previousFeedbackDate) {
return true
@ -45,159 +44,282 @@ const askFeedback = (url: string) => {
)
}
export default function PageFeedback({ customMessage }: PageFeedbackProps) {
const FeedbackButton = ({ isEmbedded }: { isEmbedded?: boolean }) => {
const [isFormOpen, setIsFormOpen] = useState(false)
const [isShowingThankMessage, setIsShowingThankMessage] = useState(false)
const [isShowingSuggestionForm, setIsShowingSuggestionForm] = useState(false)
const [isNotSatisfied, setIsNotSatisfied] = useState(false)
const { t } = useTranslation()
const url = useLocation().pathname
const currentSimulatorData = useContext(CurrentSimulatorDataContext)
const [state, setState] = useState({
showForm: false,
showThanks: false,
})
const tag = useContext(TrackingContext)
const containerRef = useRef<HTMLElement | null>(null)
const currentSimulatorData = useContext(CurrentSimulatorDataContext)
const handleFeedback = useCallback(
(rating: 'mauvais' | 'moyen' | 'bien' | 'très bien') => {
const { shouldShowRater, customTitle } = useFeedback()
useOnClickOutside(
containerRef,
() => !isShowingSuggestionForm && setIsFormOpen(false)
)
const submitFeedback = useCallback(
(rating: Feedback) => {
setFeedbackGivenForUrl(url)
tag.events.send('click.action', {
click_chapter1: 'satisfaction',
click: rating,
})
const askDetails = ['mauvais', 'moyen'].includes(rating)
setState({
showThanks: !askDetails,
showForm: askDetails,
})
const isNotSatisfiedValue = ['mauvais', 'moyen'].includes(rating)
if (isNotSatisfiedValue) {
setIsNotSatisfied(true)
}
setIsShowingThankMessage(!isNotSatisfiedValue)
setIsShowingSuggestionForm(isNotSatisfiedValue)
},
[tag, url]
)
const openSuggestionForm = useCallback(() => {
setState({ ...state, showForm: true })
}, [state])
const shouldAskFeedback = getShouldAskFeedback(url)
if (isFormOpen) {
return (
<Section ref={containerRef} $isEmbedded={isEmbedded} aria-expanded={true}>
<CloseButtonContainer>
<CloseButton
onClick={() => setIsFormOpen(false)}
aria-label={t('Fermer le module "Donner son avis"')}
>
Fermer
<svg
role="img"
aria-hidden
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.69323 17.2996C6.30271 16.9091 6.30271 16.276 6.69323 15.8854L15.8856 6.69304C16.2761 6.30252 16.9093 6.30252 17.2998 6.69304C17.6904 7.08356 17.6904 7.71673 17.2998 8.10725L8.10744 17.2996C7.71692 17.6902 7.08375 17.6902 6.69323 17.2996Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.6635 6.69306C7.05402 6.30254 7.68719 6.30254 8.07771 6.69306L17.2701 15.8854C17.6606 16.276 17.6606 16.9091 17.2701 17.2997C16.8796 17.6902 16.2464 17.6902 15.8559 17.2997L6.6635 8.10727C6.27297 7.71675 6.27297 7.08359 6.6635 6.69306Z"
/>
</svg>
</CloseButton>
</CloseButtonContainer>
{isShowingThankMessage || !shouldAskFeedback ? (
<>
<Body>
<Strong>
<Trans i18nKey="feedback.thanks">Merci de votre retour !</Trans>{' '}
<Emoji emoji="🙌" />
</Strong>
</Body>
<ThankYouText>
<Trans i18nKey="feedback.beta-testeur">
Pour continuer à donner votre avis et accéder aux nouveautés en
avant-première,{' '}
<StyledLink
href={INSCRIPTION_LINK}
aria-label="inscrivez-vous sur la liste des beta-testeur, nouvelle fenêtre"
style={{ color: '#FFF' }}
>
inscrivez-vous sur la liste des beta-testeur
</StyledLink>
</Trans>
</ThankYouText>
</>
) : (
<>
<StyledH4>
{customTitle || <Trans>Un avis sur cette page ?</Trans>}
</StyledH4>
<StyledBody>On vous écoute.</StyledBody>
<Spacing lg />
{shouldShowRater && (
<FeedbackRating submitFeedback={submitFeedback} />
)}
</>
)}
<Spacing lg />
{currentSimulatorData?.pathId === 'simulateurs.salarié' ? (
<JeDonneMonAvis />
) : (
<Button
color="tertiary"
size="XXS"
light
aria-haspopup="dialog"
onPress={() => {
setIsShowingSuggestionForm(true)
}}
>
<Trans i18nKey="feedback.reportError">Faire une suggestion</Trans>
</Button>
)}
{isShowingSuggestionForm && (
<Popover
isOpen
isDismissable
onClose={() => {
setIsShowingSuggestionForm(false)
setTimeout(() => setIsFormOpen(false))
}}
>
<FeedbackForm
isNotSatisfied={isNotSatisfied}
title={
isNotSatisfied
? t('Vos attentes ne sont pas remplies')
: t('Votre avis nous intéresse')
}
/>
</Popover>
)}
</Section>
)
}
return (
<Container>
{state.showThanks || !askFeedback(url) ? (
<>
<Body>
<Strong>
<Trans i18nKey="feedback.thanks">Merci de votre retour !</Trans>
</Strong>
</Body>
<Body>
<Trans i18nKey="feedback.beta-testeur">
Pour continuer à donner votre avis et accéder aux nouveautés en
avant-première,{' '}
<Link
href={INSCRIPTION_LINK}
aria-label="inscrivez-vous sur la liste des beta-testeur, nouvelle fenêtre"
>
inscrivez-vous sur la liste des beta-testeur
</Link>
</Trans>
</Body>
</>
) : (
<>
<SmallBody>
{customMessage || (
<Trans i18nKey="feedback.question">
Êtes-vous satisfait de cette page ?
</Trans>
)}
</SmallBody>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
}}
role="list"
>
<div role="listitem">
<EmojiButton onClick={() => handleFeedback('mauvais')}>
<Emoji
emoji="🙁"
aria-label="Pas satisfait, envoyer cette réponse"
aria-hidden={false}
/>
</EmojiButton>
</div>
<div role="listitem">
<EmojiButton onClick={() => handleFeedback('moyen')}>
<Emoji
emoji="😐"
aria-label="Moyennement satisfait, envoyer cette réponse"
aria-hidden={false}
/>
</EmojiButton>
</div>
<div role="listitem">
<EmojiButton onClick={() => handleFeedback('bien')}>
<Emoji
emoji="🙂"
aria-label="Plutôt satisfait, envoyer cette réponse"
aria-hidden={false}
/>
</EmojiButton>
</div>
<div role="listitem">
<EmojiButton onClick={() => handleFeedback('très bien')}>
<Emoji
emoji="😀"
aria-label="Très satisfait, envoyer cette réponse"
aria-hidden={false}
/>
</EmojiButton>
</div>
</div>
</>
)}
{state.showForm && (
<Popover
isOpen
title="Votre avis nous intéresse"
isDismissable
onClose={() => setState({ showThanks: true, showForm: false })}
small
>
<Form />
</Popover>
)}
<Spacing md />
<Grid container spacing={2} style={{ justifyContent: 'center' }}>
<Grid item>
{currentSimulatorData?.pathId === 'simulateurs.salarié' ? (
<JeDonneMonAvis />
) : (
<Button
onPress={openSuggestionForm}
color="tertiary"
size="XS"
light
aria-haspopup="dialog"
>
<Trans i18nKey="feedback.reportError">Faire une suggestion</Trans>
</Button>
)}
</Grid>
</Grid>
</Container>
<StyledButton
aria-label={t('Donner votre avis')}
onClick={() => setIsFormOpen(true)}
$isEmbedded={isEmbedded}
aria-haspopup="dialog"
aria-expanded={false}
>
<Emoji emoji="👋" />
</StyledButton>
)
}
const EmojiButton = styled.button`
font-size: 1.5rem;
padding: 0.6rem;
const StyledButton = styled.button<{ $isEmbedded?: boolean }>`
position: fixed;
top: 10.5rem;
${({ $isEmbedded }) => ($isEmbedded ? `top: 40rem;` : '')}
right: 0;
width: 3.75rem;
height: 3.75rem;
background-color: ${({ theme }) => theme.colors.bases.primary[700]};
border-radius: 2.5rem 0 0 2.5rem;
font-size: 1.75rem;
border: none;
background: none;
transition: transform 0.05s;
will-change: transform;
:hover {
transform: scale(1.3);
box-shadow: ${({ theme }) =>
theme.darkMode ? theme.elevationsDarkMode[2] : theme.elevations[2]};
z-index: 5;
&:hover {
background-color: ${({ theme }) => theme.colors.bases.primary[800]};
& img {
animation: wiggle 2.5s infinite;
transform-origin: 70% 70%;
}
}
@media print {
display: none;
}
@media (max-width: ${({ theme }) => theme.breakpointsWidth.md}) {
width: 3.25rem;
height: 3.25rem;
font-size: 1.5rem;
}
@keyframes wiggle {
0% {
transform: rotate(0deg);
}
10% {
transform: rotate(14deg);
} /* The following five values can be played with to make the waving more or less extreme */
20% {
transform: rotate(-8deg);
}
30% {
transform: rotate(14deg);
}
40% {
transform: rotate(-4deg);
}
50% {
transform: rotate(10deg);
}
60% {
transform: rotate(0deg);
} /* Reset for the last half to pause */
100% {
transform: rotate(0deg);
}
}
&:focus {
${FocusStyle}
}
`
const Container = styled.div`
padding: 1rem 0 1.5rem 0;
text-align: center;
const StyledH4 = styled(H4)`
margin: 0;
color: ${({ theme }) => theme.colors.extended.grey[100]};
font-size: 1rem;
`
const StyledBody = styled(Body)`
margin: 0;
`
const Section = styled.section<{ $isEmbedded?: boolean }>`
position: fixed;
top: 10.5rem;
${({ $isEmbedded }) => ($isEmbedded ? `top: 40rem;` : '')}
right: 0;
width: 17.375rem;
background-color: ${({ theme }) => theme.colors.bases.primary[700]};
border-radius: 2rem 0 0 2rem;
color: ${({ theme }) => theme.colors.extended.grey[100]};
& ${Body} {
color: ${({ theme }) => theme.colors.extended.grey[100]};
}
padding: 1.5rem;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
box-shadow: ${({ theme }) =>
theme.darkMode ? theme.elevationsDarkMode[2] : theme.elevations[2]};
z-index: 5;
@media print {
display: none;
}
`
const ThankYouText = styled(Body)`
font-size: 14px;
`
const CloseButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
background-color: transparent;
width: 100%;
margin-bottom: ${({ theme }) => theme.spacings.sm};
`
const CloseButton = styled.button`
display: flex;
align-items: center;
background-color: transparent;
border: none;
padding: 0;
color: ${({ theme }) => theme.colors.extended.grey[100]};
svg {
fill: ${({ theme }) => theme.colors.extended.grey[100]};
width: 1.5rem;
}
`
export default FeedbackButton

View File

@ -5,7 +5,6 @@ import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import PageFeedback from '@/components/Feedback'
import ShareOrSaveSimulationBanner from '@/components/ShareSimulationBanner'
import { ConversationProps } from '@/components/conversation/Conversation'
import { PopoverWithTrigger } from '@/design-system'
@ -135,22 +134,7 @@ export default function Simulation({
</FromTop>
</StyledGrid>
</Grid>
{firstStepCompleted && !hideDetails && (
<>
<div className="print-hidden">
<FadeIn>
<PageFeedback
customMessage={
<Trans i18nKey="feedback.simulator">
Êtes-vous satisfait de ce simulateur&nbsp;?
</Trans>
}
/>
</FadeIn>
</div>
{explanations}
</>
)}
{firstStepCompleted && !hideDetails && explanations}
</>
)
}

View File

@ -1,30 +1,31 @@
import { Helmet } from 'react-helmet-async'
import { Trans, useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import styled, { ThemeProvider } from 'styled-components'
import PageFeedback from '@/components/Feedback'
import FeedbackButton from '@/components/Feedback'
import LegalNotice from '@/components/LegalNotice'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
import { FooterContainer } from '@/design-system/footer'
import { FooterColumn } from '@/design-system/footer/column'
import { Container, Grid, Spacing } from '@/design-system/layout'
import { Container, Grid } from '@/design-system/layout'
import { Link } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { alternateLinks, useSitePaths } from '@/sitePaths'
import InscriptionBetaTesteur from './InscriptionBetaTesteur'
import Privacy from './Privacy'
import { useShowFeedback } from './useShowFeedback'
const hrefLangLink = alternateLinks()
export default function Footer() {
const { absoluteSitePaths } = useSitePaths()
const showFeedback = useShowFeedback()
const { t, i18n } = useTranslation()
const language = i18n.language as 'fr' | 'en'
const currentPath = useLocation().pathname
const currentEnv = import.meta.env.MODE
const encodedUri =
typeof window !== 'undefined' &&
@ -65,7 +66,7 @@ export default function Footer() {
: theme.colors.bases.tertiary[100]
}
>
{showFeedback && <PageFeedback />}
<FeedbackButton key={`${currentPath}-feedback-key`} />
{language === 'en' && (
<Body>
This website is provided by the{' '}

View File

@ -0,0 +1,49 @@
import { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import useSimulatorsData from '@/pages/Simulateurs/metadata'
import { useSitePaths } from '@/sitePaths'
const PAGE_TITLE = 'Un avis sur cette page ?'
const SIMULATOR_TITLE = 'Un avis sur ce simulateur ?'
export const useFeedback = () => {
const [shouldShowRater, setShouldShowRater] = useState(false)
const currentPath = useLocation().pathname
const currentPathDecoded = decodeURI(currentPath)
const { absoluteSitePaths } = useSitePaths()
const simulators = useSimulatorsData()
useEffect(() => {
if (
// Exclure les pages exactes
![
absoluteSitePaths.index,
'',
'/',
absoluteSitePaths.simulateurs.index,
absoluteSitePaths.plan,
absoluteSitePaths.budget,
absoluteSitePaths.accessibilité,
].includes(currentPathDecoded) &&
// Exclure les pages et sous-pages
![
absoluteSitePaths.documentation.index,
absoluteSitePaths.gérer.index,
absoluteSitePaths.créer.index,
absoluteSitePaths.nouveautés,
absoluteSitePaths.stats,
absoluteSitePaths.développeur.index,
].some((path) => currentPathDecoded.includes(path))
) {
setShouldShowRater(true)
} else {
setShouldShowRater(false)
}
}, [absoluteSitePaths, currentPathDecoded, shouldShowRater, simulators])
return {
customTitle: shouldShowRater ? SIMULATOR_TITLE : PAGE_TITLE,
shouldShowRater,
}
}

View File

@ -1,37 +0,0 @@
import { useLocation } from 'react-router-dom'
import useSimulatorsData from '@/pages/Simulateurs/metadata'
import { useSitePaths } from '@/sitePaths'
export const useShowFeedback = () => {
const currentPath = useLocation().pathname
const { absoluteSitePaths } = useSitePaths()
const simulators = useSimulatorsData()
const blacklisted = [
absoluteSitePaths.gérer.déclarationIndépendant.beta.cotisations as string,
].includes(currentPath)
if (blacklisted) {
return false
}
if (
[
simulators['déclaration-charges-sociales-indépendant'],
simulators['comparaison-statuts'],
simulators['demande-mobilité'],
]
.map((s) => s.path as string)
.includes(currentPath)
) {
return true
}
return ![
absoluteSitePaths.index,
...Object.values(simulators).map((s) => s.path),
'',
'/',
].includes(currentPath)
}

View File

@ -179,9 +179,6 @@ figure {
.print-only {
display: initial;
}
}
@media print {
.print-hidden {
display: none !important;
}

View File

@ -1,6 +1,5 @@
import { useButton } from '@react-aria/button'
import { useDialog } from '@react-aria/dialog'
import { FocusScope } from '@react-aria/focus'
import {
OverlayContainer,
OverlayProps,
@ -208,7 +207,8 @@ const PopoverContainer = styled.div<{ $offsetTop: number | null }>`
}
`}
`
const CloseButtonContainer = styled.div`
export const CloseButtonContainer = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.colors.extended.grey[300]};
display: flex;
@ -216,7 +216,7 @@ const CloseButtonContainer = styled.div`
height: ${({ theme }) => theme.spacings.xxl};
justify-content: flex-end;
`
const CloseButton = styled.button`
export const CloseButton = styled.button`
display: inline-flex;
align-items: center;

View File

@ -1,17 +1,11 @@
import { useOverlayTrigger } from '@react-aria/overlays'
import { useOverlayTriggerState } from '@react-stately/overlays'
import { AriaButtonProps } from '@react-types/button'
import React, {
ReactElement,
Ref,
RefObject,
useEffect,
useMemo,
useRef,
} from 'react'
import React, { ReactElement, Ref, RefObject, useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import { Button } from '@/design-system/buttons'
import { omit } from '@/utils'
import { Link } from '../typography/link'
import Popover from './Popover'
@ -45,17 +39,13 @@ export default function PopoverWithTrigger({
openButtonRef
)
const triggerButton = useMemo(
() =>
trigger({
onPress: () => {
state.open()
},
ref: openButtonRef,
...triggerProps,
}),
[openButtonRef, triggerProps, trigger, state]
)
const triggerButton = trigger({
onPress: () => {
state.open()
},
ref: openButtonRef,
...omit(triggerProps, 'onPress'),
})
const { pathname } = useLocation()
const pathnameRef = useRef(pathname)
@ -73,14 +63,18 @@ export default function PopoverWithTrigger({
<Popover
{...overlayProps}
title={title}
onClose={() => state.close()}
onClose={() => {
state.close()
}}
isDismissable
role="dialog"
small={small}
contentRef={contentRef}
>
{typeof children === 'function'
? children(() => state.close())
? children(() => {
state.close()
})
: children}
</Popover>
)}

View File

@ -0,0 +1,29 @@
import { RefObject, useEffect } from 'react'
type Event = MouseEvent | TouchEvent
export const useOnClickOutside = (
ref: RefObject<HTMLElement>,
handler: (event: Event | null) => void
) => {
useEffect(() => {
const listener = (event: Event | null) => {
// Do nothing if clicking ref's element or descendent elements
if (
!ref.current ||
(event?.target instanceof HTMLElement &&
ref.current.contains(event.target))
) {
return
}
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}

View File

@ -1,3 +1,4 @@
import FeedbackButton from '@/components/Feedback'
import Privacy from '@/components/layout/Footer/Privacy'
import { Spacing } from '@/design-system/layout'
@ -9,6 +10,7 @@ export default function IframeFooter() {
textAlign: 'center',
}}
>
<FeedbackButton isEmbedded />
<Spacing xl />
<Privacy />
<Spacing lg />

View File

@ -2,7 +2,6 @@ import { useContext } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Navigate } from 'react-router-dom'
import PageFeedback from '@/components/Feedback'
import { FromBottom } from '@/components/ui/animate'
import DefaultHelmet from '@/components/utils/DefaultHelmet'
import { ScrollToTop } from '@/components/utils/Scroll'
@ -142,9 +141,7 @@ export default function VotreSituation() {
</>
)}
</section>
<PageFeedback
customMessage={<Trans>Êtes vous satisfait de cet assistant ?</Trans>}
/>
<SmallBody>
<Emoji emoji="🏗️" />{' '}
<Trans i18nKey="économieCollaborative.WIP">

View File

@ -1,5 +1,4 @@
import Value, { Condition, WhenAlreadyDefined } from '@/components/EngineValue'
import PageFeedback from '@/components/Feedback'
import ShareOrSaveSimulationBanner from '@/components/ShareSimulationBanner'
import Conversation from '@/components/conversation/Conversation'
import Progress from '@/components/ui/Progress'
@ -143,16 +142,6 @@ export default function Cotisations() {
<Spacing xl />
</FromTop>
</Container>
<Container
backgroundColor={(theme) =>
theme.darkMode
? theme.colors.extended.dark[600]
: theme.colors.bases.tertiary[100]
}
>
<PageFeedback customMessage="Qu'avez-vous pensé de cet assistant ?" />
</Container>
</WhenAlreadyDefined>
</FromTop>
)