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 modulepull/2444/head
parent
0fd1115aad
commit
5584fd1242
|
@ -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')}
|
||||
|
|
|
@ -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 n’avez 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;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 ?
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</FadeIn>
|
||||
</div>
|
||||
{explanations}
|
||||
</>
|
||||
)}
|
||||
{firstStepCompleted && !hideDetails && explanations}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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{' '}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -179,9 +179,6 @@ figure {
|
|||
.print-only {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.print-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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])
|
||||
}
|
|
@ -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 />
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue