Améliore le style des références et ajoute un feedback sur la page résultat

pull/2757/head
Johan Girod 2023-08-03 16:13:48 +02:00
parent 8d635bdbbb
commit 9c49a34feb
8 changed files with 258 additions and 227 deletions

View File

@ -0,0 +1,168 @@
import { useCallback, useContext, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import { TrackingContext } from '@/components/ATInternetTracking'
import { Popover } 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 { H4 } from '@/design-system/typography/heading'
import { StyledLink } from '@/design-system/typography/link'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import { useSitePaths } from '@/sitePaths'
import * as safeLocalStorage from '../../storage/safeLocalStorage'
import { JeDonneMonAvis } from '../JeDonneMonAvis'
import { INSCRIPTION_LINK } from '../layout/Footer/InscriptionBetaTesteur'
import FeedbackForm from './FeedbackForm'
import FeedbackRating, { FeedbackT } from './FeedbackRating'
import { useFeedback } from './useFeedback'
const localStorageKey = (url: string) => `app::feedback::v3::${url}`
const setFeedbackGivenForUrl = (url: string) => {
safeLocalStorage.setItem(
localStorageKey(url),
JSON.stringify(new Date().toISOString())
)
}
// Ask for feedback again after 4 months
const getShouldAskFeedback = (url: string) => {
const previousFeedbackDate = safeLocalStorage.getItem(localStorageKey(url))
if (!previousFeedbackDate) {
return true
}
return (
new Date(previousFeedbackDate) <
new Date(new Date().setMonth(new Date().getMonth() - 4))
)
}
const IFRAME_SIMULATEUR_EMBAUCHE_PATH = '/iframes/simulateur-embauche'
export function Feedback({
onEnd,
onFeedbackFormOpen,
}: {
onEnd?: () => void
onFeedbackFormOpen?: () => void
}) {
const [isShowingThankMessage, setIsShowingThankMessage] = useState(false)
const [isShowingSuggestionForm, setIsShowingSuggestionForm] = useState(false)
const [isNotSatisfied, setIsNotSatisfied] = useState(false)
const { t } = useTranslation()
const url = useLocation().pathname
const tag = useContext(TrackingContext)
const { absoluteSitePaths } = useSitePaths()
const currentPath = useLocation().pathname
const isSimulateurSalaire =
currentPath.includes(absoluteSitePaths.simulateurs.salarié) ||
currentPath.includes(IFRAME_SIMULATEUR_EMBAUCHE_PATH)
const { shouldShowRater, customTitle } = useFeedback()
const submitFeedback = useCallback(
(rating: FeedbackT) => {
setFeedbackGivenForUrl(url)
tag.events.send('click.action', {
click_chapter1: 'satisfaction',
click: rating,
})
const isNotSatisfiedValue = ['mauvais', 'moyen'].includes(rating)
if (isNotSatisfiedValue) {
setIsNotSatisfied(true)
onFeedbackFormOpen?.()
}
setIsShowingThankMessage(!isNotSatisfiedValue)
setIsShowingSuggestionForm(isNotSatisfiedValue)
},
[tag, url]
)
const shouldAskFeedback = getShouldAskFeedback(url)
return (
<>
{isShowingThankMessage || !shouldAskFeedback ? (
<>
<Body>
<Strong>
<Trans i18nKey="feedback.thanks">Merci de votre retour !</Trans>{' '}
<Emoji emoji="🙌" />
</Strong>
</Body>
<SmallBody>
<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"
>
inscrivez-vous sur la liste des beta-testeur
</StyledLink>
</Trans>
</SmallBody>
</>
) : (
<>
<H4>{customTitle || <Trans>Un avis sur cette page ?</Trans>}</H4>
{shouldShowRater && (
<FeedbackRating submitFeedback={submitFeedback} />
)}
</>
)}
<Spacing lg />
{isSimulateurSalaire ? (
<JeDonneMonAvis light />
) : (
<Button
color="tertiary"
size="XXS"
light
aria-haspopup="dialog"
onPress={() => {
setIsShowingSuggestionForm(true)
onFeedbackFormOpen?.()
}}
>
<Trans i18nKey="feedback.reportError">Faire une suggestion</Trans>
</Button>
)}
{isShowingSuggestionForm && (
<Popover
isOpen
isDismissable
onClose={() => {
setIsShowingSuggestionForm(false)
setTimeout(() => onEnd?.())
}}
title={
isNotSatisfied
? t('Vos attentes ne sont pas remplies')
: t('Votre avis nous intéresse')
}
>
<FeedbackForm
infoSlot={
isNotSatisfied && (
<Body>
<Trans>
Vous navez pas été satisfait(e) de votre expérience, nous
en sommes désolé(e)s.
</Trans>
</Body>
)
}
/>
</Popover>
)}
</>
)
}

View File

@ -22,8 +22,6 @@ type SubmitError = {
const SHORT_MAX_LENGTH = 254
const FeedbackThankYouContent = () => {
const { t } = useTranslation()
return (
<>
<StyledEmojiContainer role="img" aria-hidden>
@ -51,14 +49,12 @@ const FeedbackThankYouContent = () => {
}
export default function FeedbackForm({
title,
infoSlot,
description,
placeholder,
tags,
hideShare,
}: {
title: string
infoSlot?: ReactNode
description?: ReactNode
placeholder?: string
@ -129,10 +125,6 @@ export default function FeedbackForm({
{isSubmittedSuccessfully && <FeedbackThankYouContent />}
{!isSubmittedSuccessfully && (
<>
<H1 as="h4" style={{ marginTop: '1rem' }}>
{title}
</H1>
<StyledFeedback>
<form
onSubmit={(e) => {

View File

@ -2,12 +2,12 @@ import styled from 'styled-components'
import { Emoji } from '@/design-system/emoji'
export type Feedback = 'mauvais' | 'moyen' | 'bien' | 'très bien'
export type FeedbackT = 'mauvais' | 'moyen' | 'bien' | 'très bien'
const FeedbackRating = ({
submitFeedback,
}: {
submitFeedback: (feedbackValue: Feedback) => void
submitFeedback: (feedbackValue: FeedbackT) => void
}) => {
return (
<div

View File

@ -1,103 +1,27 @@
import FocusTrap from 'focus-trap-react'
import {
MutableRefObject,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import { MutableRefObject, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { TrackingContext } from '@/components/ATInternetTracking'
import { Popover } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
import { FocusStyle } from '@/design-system/global-style'
import { Spacing } from '@/design-system/layout'
import { Strong } from '@/design-system/typography'
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/useOnClickOutside'
import { useSitePaths } from '@/sitePaths'
import * as safeLocalStorage from '../../storage/safeLocalStorage'
import { JeDonneMonAvis } from '../JeDonneMonAvis'
import { INSCRIPTION_LINK } from '../layout/Footer/InscriptionBetaTesteur'
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) => {
safeLocalStorage.setItem(
localStorageKey(url),
JSON.stringify(new Date().toISOString())
)
}
// Ask for feedback again after 4 months
const getShouldAskFeedback = (url: string) => {
const previousFeedbackDate = safeLocalStorage.getItem(localStorageKey(url))
if (!previousFeedbackDate) {
return true
}
return (
new Date(previousFeedbackDate) <
new Date(new Date().setMonth(new Date().getMonth() - 4))
)
}
const IFRAME_SIMULATEUR_EMBAUCHE_PATH = '/iframes/simulateur-embauche'
import { ForceThemeProvider } from '../utils/DarkModeContext'
import { Feedback } from './Feedback'
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 tag = useContext(TrackingContext)
const containerRef = useRef<HTMLElement | null>(null)
const { absoluteSitePaths } = useSitePaths()
const currentPath = useLocation().pathname
const isSimulateurSalaire =
currentPath.includes(absoluteSitePaths.simulateurs.salarié) ||
currentPath.includes(IFRAME_SIMULATEUR_EMBAUCHE_PATH)
const { shouldShowRater, customTitle } = useFeedback()
const [feedbackFormIsOpened, setFeedbackFormIsOpened] = useState(false)
useOnClickOutside(
containerRef,
() => !isShowingSuggestionForm && setIsFormOpen(false)
)
const submitFeedback = useCallback(
(rating: Feedback) => {
setFeedbackGivenForUrl(url)
tag.events.send('click.action', {
click_chapter1: 'satisfaction',
click: rating,
})
const isNotSatisfiedValue = ['mauvais', 'moyen'].includes(rating)
if (isNotSatisfiedValue) {
setIsNotSatisfied(true)
}
setIsShowingThankMessage(!isNotSatisfiedValue)
setIsShowingSuggestionForm(isNotSatisfiedValue)
},
[tag, url]
() => !feedbackFormIsOpened && setIsFormOpen(false)
)
const buttonRef = useRef() as MutableRefObject<HTMLButtonElement | null>
const shouldAskFeedback = getShouldAskFeedback(url)
const handleClose = () => {
setIsFormOpen(false)
setTimeout(() => {
@ -124,7 +48,7 @@ const FeedbackButton = ({ isEmbedded }: { isEmbedded?: boolean }) => {
return (
<Section ref={containerRef} $isEmbedded={isEmbedded} aria-expanded={true}>
<FocusTrap>
<div>
<ForceThemeProvider forceTheme="dark">
<CloseButtonContainer>
<CloseButton
onClick={handleClose}
@ -152,95 +76,17 @@ const FeedbackButton = ({ isEmbedded }: { isEmbedded?: boolean }) => {
</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 />
{isSimulateurSalaire ? (
<JeDonneMonAvis light />
) : (
<Button
color="tertiary"
size="XXS"
light
aria-haspopup="dialog"
onPress={() => {
setIsShowingSuggestionForm(true)
}}
>
<Trans i18nKey="feedback.reportError">
Faire une suggestion
</Trans>
</Button>
)}
</div>
</FocusTrap>
{isShowingSuggestionForm && (
<Popover
isOpen
isDismissable
onClose={() => {
setIsShowingSuggestionForm(false)
setTimeout(() => setIsFormOpen(false))
}}
title={
isNotSatisfied
? t('Vos attentes ne sont pas remplies')
: t('Votre avis nous intéresse')
}
>
<FeedbackForm
infoSlot={
isNotSatisfied && (
<Body>
<Trans>
Vous navez pas été satisfait(e) de votre expérience, nous
en sommes désolé(e)s.
</Trans>
</Body>
)
}
title={
isNotSatisfied
? t('Vos attentes ne sont pas remplies')
: t('Votre avis nous intéresse')
}
<Feedback
onEnd={() => {
if (!feedbackFormIsOpened) {
setIsFormOpen(false)
}
setFeedbackFormIsOpened(false)
}}
onFeedbackFormOpen={() => setFeedbackFormIsOpened(true)}
/>
</Popover>
)}
</ForceThemeProvider>
</FocusTrap>
</Section>
)
}
@ -324,16 +170,6 @@ const StyledButton = styled.button<{
}
`
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;
@ -342,10 +178,7 @@ const Section = styled.section<{ $isEmbedded?: boolean }>`
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;
padding-top: 0.75rem;
display: flex;
@ -360,10 +193,6 @@ const Section = styled.section<{ $isEmbedded?: boolean }>`
}
`
const ThankYouText = styled(Body)`
font-size: 14px;
`
const CloseButtonContainer = styled.div`
display: flex;
justify-content: flex-end;

View File

@ -26,12 +26,15 @@ export const useFeedback = () => {
absoluteSitePaths.budget,
absoluteSitePaths.assistants.index,
absoluteSitePaths.assistants['choix-du-statut'].index,
absoluteSitePaths.assistants['choix-du-statut']['recherche-activité'],
absoluteSitePaths.assistants['choix-du-statut']['détails-activité'],
absoluteSitePaths.accessibilité,
].includes(currentPathDecoded) &&
// Exclure les pages et sous-pages
![
absoluteSitePaths.documentation.index,
absoluteSitePaths.nouveautés.index,
absoluteSitePaths.stats,
absoluteSitePaths.développeur.index,
].some((path) => currentPathDecoded.includes(path))

View File

@ -4,6 +4,7 @@ import { useContext } from 'react'
import styled from 'styled-components'
import { EngineContext, useEngine } from '@/components/utils/EngineContext'
import { Grid } from '@/design-system/layout'
import { Link } from '@/design-system/typography/link'
import { Li, Ul } from '@/design-system/typography/list'
import { capitalise0 } from '@/utils'
@ -55,26 +56,38 @@ function Reference({ href, title }: { href: string; title: string }) {
return (
<Li key={href}>
<Link
href={href}
<Grid
container
spacing={2}
css={`
display: flex;
display: inline-flex;
`}
>
<div
<Grid item xs={12} sm="auto">
<Link
href={href}
css={`
display: flex;
`}
>
{capitalise0(title)}
</Link>
</Grid>
<Grid
item
xs="auto"
css={`
flex: 1;
text-align: right;
`}
>
{capitalise0(title)}
</div>
{domain in referencesImages && (
<StyledImage
src={referencesImages[domain as keyof typeof referencesImages]}
alt={`logo du site ${domain}`}
/>
)}
</Link>
{domain in referencesImages && (
<StyledImage
src={referencesImages[domain as keyof typeof referencesImages]}
alt={`logo du site ${domain}`}
/>
)}
</Grid>
</Grid>
</Li>
)
}
@ -89,7 +102,7 @@ const StyledImage = styled.img`
border-radius: ${({ theme }) => theme.box.borderRadius};
background-color: ${({ theme }) => theme.colors.extended.grey[100]};
max-height: 2.5rem;
max-height: 2.25rem;
`
const referencesImages = {
'service-public.fr': '/références-images/service-public.png',

View File

@ -5,9 +5,11 @@ import { useLocation } from 'react-router-dom'
import { TrackPage } from '@/components/ATInternetTracking'
import { CurrentSimulatorCard } from '@/components/CurrentSimulatorCard'
import { Feedback } from '@/components/Feedback/Feedback'
import { References } from '@/components/References'
import { StatutType } from '@/components/StatutTag'
import { useEngine } from '@/components/utils/EngineContext'
import { Message } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Article } from '@/design-system/card'
import { Emoji } from '@/design-system/emoji'
@ -44,7 +46,7 @@ export default function Résultat() {
nécessaires à la création de votre entreprise. Voici quelques pistes.
</Intro>
<Grid spacing={3} container>
<Grid item lg={6} sm={12}>
<Grid item xl={4} lg={6} sm={12}>
<Article
title="Le guide complet pour créer son activité"
href="https://entreprendre.service-public.fr/vosdroits/N31901"
@ -54,7 +56,7 @@ export default function Résultat() {
entreprise, du stade de l'idée au lancement de l'entreprise.
</Article>
</Grid>
<Grid item lg={6} sm={12}>
<Grid item xl={4} lg={6} sm={12}>
<Article
title="Vos démarches en ligne"
href="https://formalites.entreprises.gouv.fr/"
@ -64,17 +66,41 @@ export default function Résultat() {
gratuite, sur le site officiel formalites.entreprises.gouv.fr.
</Article>
</Grid>
<Grid item xl={4} xs={12} sm>
<Message
type="info"
border={false}
css={`
text-align: center;
display: flex;
align-items: center;
height: 100%;
`}
>
<Feedback />
<Spacing sm />
</Message>
</Grid>
<Grid
item
xs
css={`
text-align: right;
`}
xl="auto"
>
<Button
color="secondary"
light
size="XXS"
to={absoluteSitePaths.assistants['choix-du-statut'].index}
>
<span aria-hidden></span> Recommencer l'assistant
</Button>
</Grid>
</Grid>
<Spacing xl />
<Button
color="secondary"
light
size="XS"
to={absoluteSitePaths.assistants['choix-du-statut'].index}
>
<span aria-hidden></span> Recommencer l'assistant
</Button>
<Spacing xl />
<Spacing md />
<Container
backgroundColor={(theme) =>
theme.darkMode

View File

@ -19,6 +19,7 @@ export default function ActivityNotFound({ job }: { job: string }) {
return (
<>
<PopoverWithTrigger
title={t('Quelle est votre activité ?')}
trigger={(buttonProps) =>
// eslint-disable-next-line react/jsx-props-no-spreading
hide ? (
@ -43,7 +44,6 @@ export default function ActivityNotFound({ job }: { job: string }) {
{() => (
<>
<FeedbackForm
title={t('Quelle est votre activité ?')}
infoSlot={
<Message border={false} type="info" icon>
<Trans i18nKey="search-code-ape.feedback.info">