From 91f32f4d97e983f24daf59e682958377247e9d90 Mon Sep 17 00:00:00 2001 From: mariana-caetano <67270558+mariana-caetano@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:50:05 -0300 Subject: [PATCH 1/5] chore(i18n): add follow-up feedback message keys --- src/messages/en.json | 6 ++++++ src/messages/es.json | 6 ++++++ src/messages/pt.json | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/src/messages/en.json b/src/messages/en.json index 3fe55fc..ea2b7cf 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -4,6 +4,12 @@ "feedback_section.positive": "Yes", "feedback_section.negative": "No", "feedback_section.edit": "Suggest Edits (GitHub)", + "feedback_section.followup_placeholder": "Tell us more (optional)", + "feedback_section.followup_email_placeholder": "Leave your email if you'd like a reply (optional)", + "feedback_section.followup_send": "Send", + "feedback_section.followup_skip": "Skip", + "feedback_section.followup_success": "Thanks for the detailed feedback!", + "feedback_section.followup_error": "We couldn't send your feedback. Please try again.", "feedback_modal.button": "Send feedback", "feedback_modal.title": "Send feedback", "feedback_modal.description": "Share suggestions, issues found, or ideas to improve our documentation.", diff --git a/src/messages/es.json b/src/messages/es.json index f8d2322..fccfb55 100644 --- a/src/messages/es.json +++ b/src/messages/es.json @@ -4,6 +4,12 @@ "feedback_section.positive": "Sí", "feedback_section.negative": "No", "feedback_section.edit": "Sugerir cambios (GitHub)", + "feedback_section.followup_placeholder": "Cuéntanos más (opcional)", + "feedback_section.followup_email_placeholder": "Deja tu email si quieres una respuesta (opcional)", + "feedback_section.followup_send": "Enviar", + "feedback_section.followup_skip": "Omitir", + "feedback_section.followup_success": "¡Gracias por el detalle!", + "feedback_section.followup_error": "No pudimos enviar tu feedback. Inténtalo de nuevo.", "feedback_modal.button": "Enviar feedback", "feedback_modal.title": "Enviar feedback", "feedback_modal.description": "Comparte sugerencias, problemas encontrados o ideas para mejorar nuestra documentación.", diff --git a/src/messages/pt.json b/src/messages/pt.json index 7080569..165b641 100644 --- a/src/messages/pt.json +++ b/src/messages/pt.json @@ -4,6 +4,12 @@ "feedback_section.positive": "Sim", "feedback_section.negative": "Não", "feedback_section.edit": "Sugerir edições (GitHub)", + "feedback_section.followup_placeholder": "Conte mais (opcional)", + "feedback_section.followup_email_placeholder": "Deixe seu email se quiser retorno (opcional)", + "feedback_section.followup_send": "Enviar", + "feedback_section.followup_skip": "Pular", + "feedback_section.followup_success": "Obrigado pelo detalhamento!", + "feedback_section.followup_error": "Não foi possível enviar. Tente novamente.", "feedback_modal.button": "Enviar feedback", "feedback_modal.title": "Enviar feedback", "feedback_modal.description": "Compartilhe sugestões, problemas encontrados ou ideias para melhorar nossa documentação.", From 4e001c416e57cfcd396d7fe4557bc719fa41d348 Mon Sep 17 00:00:00 2001 From: mariana-caetano <67270558+mariana-caetano@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:10:34 -0300 Subject: [PATCH 2/5] style(feedback-section): add styles for follow-up panel --- src/components/feedback-section/styles.ts | 160 ++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/components/feedback-section/styles.ts b/src/components/feedback-section/styles.ts index cd89e4b..90f6c7a 100644 --- a/src/components/feedback-section/styles.ts +++ b/src/components/feedback-section/styles.ts @@ -2,6 +2,11 @@ import { SxStyleProp } from '@vtex/brand-ui' type StyleFn = (opts?: { small?: boolean }) => SxStyleProp +const wrapper: StyleFn = () => ({ + width: '100%', + flexDirection: 'column', +}) + const container: StyleFn = ({ small } = {}) => ({ width: '100%', flexDirection: small ? 'column' : ['column', 'row'], @@ -73,8 +78,151 @@ const disabled: SxStyleProp = { const shareButton: SxStyleProp = {} +const panelControl: SxStyleProp = { + width: '100%', + boxSizing: 'border-box', + padding: '10px 12px', + border: '1px solid #E7E9EE', + borderRadius: '8px', + background: '#FFFFFF', + color: '#142032', + fontFamily: 'inherit', + fontSize: '14px', + lineHeight: '20px', + outline: 'none', + transition: + 'border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease', + '::placeholder': { + color: '#A1A8B3', + }, + ':hover': { + borderColor: '#C7CDD6', + }, + ':focus': { + borderColor: '#E31C58', + boxShadow: '0 0 0 3px rgba(227, 28, 88, 0.16)', + }, +} + +const panel: StyleFn = ({ small } = {}) => ({ + width: '100%', + marginTop: small ? '8px' : '12px', + padding: small ? '12px' : '16px', + border: '1px solid #E7E9EE', + borderRadius: '8px', + background: '#FAFAFB', + display: 'flex', + flexDirection: 'column', + gap: '10px', +}) + +const panelForm: SxStyleProp = { + display: 'flex', + flexDirection: 'column', + gap: '10px', +} + +const panelTextarea: SxStyleProp = { + ...panelControl, + minHeight: '80px', + resize: 'vertical', +} + +const panelEmailInput: SxStyleProp = { + ...panelControl, + height: '38px', +} + +const panelActions: SxStyleProp = { + display: 'flex', + gap: '8px', + alignItems: 'center', + justifyContent: 'flex-end', +} + +const panelSkipButton: SxStyleProp = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '36px', + px: '14px', + background: 'transparent', + color: '#4A596B', + border: '1px solid #E7E9EE', + borderRadius: '8px', + cursor: 'pointer', + fontFamily: 'inherit', + fontSize: '13px', + fontWeight: '600', + ':hover': { + background: '#F8F7FC', + color: '#142032', + }, +} + +const panelSendButton: SxStyleProp = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '36px', + px: '14px', + background: '#142032', + color: '#FFFFFF', + border: 'none', + borderRadius: '8px', + cursor: 'pointer', + fontFamily: 'inherit', + fontSize: '13px', + fontWeight: '600', + transition: 'background 0.15s ease, opacity 0.15s ease', + ':hover': { + background: '#000711', + }, + ':disabled': { + opacity: 0.6, + cursor: 'not-allowed', + }, +} + +const panelSuccess: SxStyleProp = { + alignItems: 'center', + gap: '8px', +} + +const panelSuccessIcon: SxStyleProp = { + flexShrink: 0, + color: '#36875A', +} + +const panelSuccessText: SxStyleProp = { + fontSize: '13px', + color: '#142032', +} + +const panelErrorText: SxStyleProp = { + display: 'flex', + alignItems: 'flex-start', + gap: '8px', + padding: '10px 12px', + background: '#FFF6F4', + border: '1px solid #F8D0C8', + borderRadius: '8px', + color: '#8A1F11', + fontSize: '13px', + lineHeight: '18px', +} + +const panelErrorIcon: SxStyleProp = { + flexShrink: 0, + width: '16px', + height: '16px', + mt: '1px', + color: '#D44333', +} + export default { disabled, + wrapper, container, question, likeContainer, @@ -85,4 +233,16 @@ export default { selectedButton, shareButton, iconsContainer, + panel, + panelForm, + panelTextarea, + panelEmailInput, + panelActions, + panelSkipButton, + panelSendButton, + panelSuccess, + panelSuccessIcon, + panelSuccessText, + panelErrorText, + panelErrorIcon, } From 26a7839eadb9b02c3ed476a3328d850df530be67 Mon Sep 17 00:00:00 2001 From: mariana-caetano <67270558+mariana-caetano@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:11:18 -0300 Subject: [PATCH 3/5] feat(feedback-section): open follow-up panel after vote --- src/components/feedback-section/index.tsx | 316 +++++++++++++++++----- 1 file changed, 252 insertions(+), 64 deletions(-) diff --git a/src/components/feedback-section/index.tsx b/src/components/feedback-section/index.tsx index a13d949..a7dc2df 100644 --- a/src/components/feedback-section/index.tsx +++ b/src/components/feedback-section/index.tsx @@ -1,7 +1,9 @@ -import { Flex, Text } from '@vtex/brand-ui' +import { ChangeEvent, FormEvent, useEffect, useRef, useState } from 'react' +import { Box, Flex, Text } from '@vtex/brand-ui' import LikeIcon from 'components/icons/like-icon' import LikeSelectedIcon from 'components/icons/like-selected-icon' -import { useEffect, useState } from 'react' +import CheckIcon from 'components/icons/check-icon' +import WarningIcon from 'components/icons/warning-icon' import styles from './styles' import { useLocale } from 'utils/context/libraryContext' import { messages } from 'utils/get-message' @@ -9,6 +11,17 @@ import ShareButton from 'components/share-button' import SuggestEdits from 'components/suggest-edits' const DEFAULT_FEEDBACK_ENDPOINT = '/api/feedback/' +const DEFAULT_DETAILED_FEEDBACK_ENDPOINT = '/api/feedback/' + +export type DetailedFeedbackPayload = { + /** 'positive' | 'negative', matching the original vote. */ + type: string + /** Optional free-text comment left by the user. */ + feedback: string + /** Optional email left by the user for a follow-up. */ + email: string + url: string +} export interface FeedbackSectionProps { /** Slug that corresponds to the current page. */ @@ -31,11 +44,28 @@ export interface FeedbackSectionProps { sendFeedback?: (liked: boolean) => Promise /** Whether to render the small version of the component or not. */ small?: boolean + /** + * Whether to show the inline follow-up panel (optional comment + email) + * right after the user votes. Defaults to `true`. + */ + collectDetailedFeedback?: boolean + /** + * Endpoint that receives the detailed follow-up payload (comment + email). + * Appends a second row to the same spreadsheet as the vote. + * Defaults to `/api/feedback/`. + */ + detailedFeedbackEndpoint?: string + /** Override the default POST for the detailed follow-up. */ + sendDetailedFeedback?: (payload: DetailedFeedbackPayload) => Promise + /** Force the initial state of the follow-up panel. Useful for Storybook. */ + defaultPanelStage?: PanelStage } /** @deprecated Use FeedbackSectionProps */ export type DocPath = FeedbackSectionProps +type PanelStage = 'closed' | 'open' | 'submitted' | 'error' + const postFeedback = async ( liked: boolean, pageUrl: string, @@ -53,6 +83,30 @@ const postFeedback = async ( }) } +const postDetailedFeedback = async ( + payload: DetailedFeedbackPayload, + endpoint: string +) => { + // Appends a second, independent row to the same spreadsheet used for the + // vote, keeping the same column convention: timestamp, url, sentiment, + // plus the optional comment and email in the following columns. + const res = await fetch(endpoint, { + method: 'POST', + body: JSON.stringify({ + data: [ + new Date().toISOString(), + payload.url, + payload.type, + payload.feedback, + payload.email, + ], + }), + }) + if (!res.ok) { + throw new Error('Failed to send detailed feedback') + } +} + const FeedbackSection = ({ slug, urlToEdit, @@ -62,19 +116,38 @@ const FeedbackSection = ({ feedbackEndpoint = DEFAULT_FEEDBACK_ENDPOINT, sendFeedback, small = false, + collectDetailedFeedback = true, + detailedFeedbackEndpoint = DEFAULT_DETAILED_FEEDBACK_ENDPOINT, + sendDetailedFeedback, + defaultPanelStage = 'closed', }: FeedbackSectionProps) => { const [feedback, setFeedback] = useState(undefined) + const [panelStage, setPanelStage] = useState(defaultPanelStage) + const [comment, setComment] = useState('') + const [email, setEmail] = useState('') + const [submitting, setSubmitting] = useState(false) + const textareaRef = useRef(null) const locale = useLocale() useEffect(() => { setFeedback(undefined) + setPanelStage('closed') + setComment('') + setEmail('') }, [slug]) + useEffect(() => { + if (panelStage === 'open') { + textareaRef.current?.focus() + } + }, [panelStage]) + + const resolvedPageUrl = + pageUrl ?? (typeof window !== 'undefined' ? window.location.href : '') + const handleSend = async (liked: boolean) => { if (feedback !== undefined) return setFeedback(liked) - const resolvedPageUrl = - pageUrl ?? (typeof window !== 'undefined' ? window.location.href : '') try { if (sendFeedback) { await sendFeedback(liked) @@ -83,74 +156,189 @@ const FeedbackSection = ({ } } catch (e) { setFeedback(undefined) + return + } + + if (collectDetailedFeedback) { + setPanelStage('open') + } + } + + const handleSkip = () => { + setPanelStage('closed') + } + + const handleSubmitDetails = async (event: FormEvent) => { + event.preventDefault() + if (!comment.trim() && !email.trim()) { + setPanelStage('closed') + return + } + + setSubmitting(true) + const payload: DetailedFeedbackPayload = { + type: feedback === true ? 'positive' : 'negative', + feedback: comment.trim(), + email: email.trim(), + url: resolvedPageUrl, + } + + try { + if (sendDetailedFeedback) { + await sendDetailedFeedback(payload) + } else { + await postDetailedFeedback(payload, detailedFeedbackEndpoint) + } + setPanelStage('submitted') + } catch (e) { + setPanelStage('error') + } finally { + setSubmitting(false) } } return ( - - - - {feedback !== undefined - ? messages[locale]['feedback_section.response'] - : messages[locale]['feedback_section.question']} - - - - handleSend(true)} - role="button" - aria-pressed={feedback === true} - data-cy="feedback-section-like" - > - {feedback === true ? ( - - ) : ( - - )} - {!small && ( - {messages[locale]['feedback_section.positive']} - )} - + + + + + {feedback !== undefined + ? messages[locale]['feedback_section.response'] + : messages[locale]['feedback_section.question']} + - handleSend(false)} - role="button" - aria-pressed={feedback === false} - data-cy="feedback-section-dislike" - > - {feedback === false ? ( - - ) : ( - - )} - {!small && ( - {messages[locale]['feedback_section.negative']} - )} + + handleSend(true)} + role="button" + aria-pressed={feedback === true} + data-cy="feedback-section-like" + > + {feedback === true ? ( + + ) : ( + + )} + {!small && ( + {messages[locale]['feedback_section.positive']} + )} + + + handleSend(false)} + role="button" + aria-pressed={feedback === false} + data-cy="feedback-section-dislike" + > + {feedback === false ? ( + + ) : ( + + )} + {!small && ( + {messages[locale]['feedback_section.negative']} + )} + + {suggestEdits && urlToEdit && ( + + )} + {shareButton && ( + + )} - {suggestEdits && urlToEdit && ( - - )} - {shareButton && ( - + + {collectDetailedFeedback && panelStage !== 'closed' && ( + + {panelStage === 'submitted' ? ( + + + + {messages[locale]['feedback_section.followup_success']} + + + ) : ( + + {panelStage === 'error' && ( + + + {messages[locale]['feedback_section.followup_error']} + + )} + ) => + setComment(e.target.value) + } + rows={small ? 3 : 4} + placeholder={ + messages[locale]['feedback_section.followup_placeholder'] + } + sx={styles.panelTextarea} + data-cy="feedback-section-followup-textarea" + /> + ) => + setEmail(e.target.value) + } + placeholder={ + messages[locale][ + 'feedback_section.followup_email_placeholder' + ] + } + sx={styles.panelEmailInput} + data-cy="feedback-section-followup-email" + /> + + + {messages[locale]['feedback_section.followup_skip']} + + + {messages[locale]['feedback_section.followup_send']} + + + + )} + )} ) From d88a4321e52b0b1cd4fcf1034e3af4362c448920 Mon Sep 17 00:00:00 2001 From: mariana-caetano <67270558+mariana-caetano@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:11:35 -0300 Subject: [PATCH 4/5] test(feedback-section): add storybook stories for follow-up states --- .../FeedbackSection.stories.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/components/feedback-section/FeedbackSection.stories.tsx b/src/components/feedback-section/FeedbackSection.stories.tsx index 20aefcc..e5c330c 100644 --- a/src/components/feedback-section/FeedbackSection.stories.tsx +++ b/src/components/feedback-section/FeedbackSection.stories.tsx @@ -92,3 +92,35 @@ export const Portuguese: Story = { locale: 'pt', }, } + +/** Follow-up panel opened automatically right after voting "Yes". */ +export const FollowUpOpen: Story = { + args: { + defaultPanelStage: 'open', + }, +} + +/** Follow-up panel after the user sends the optional comment/email. */ +export const FollowUpSubmitted: Story = { + args: { + defaultPanelStage: 'submitted', + }, +} + +/** Follow-up panel when sending the detailed feedback fails. */ +export const FollowUpError: Story = { + args: { + defaultPanelStage: 'open', + sendDetailedFeedback: async () => { + throw new Error('Network error') + }, + }, +} + +/** Compact version of the follow-up panel used next to the table of contents. */ +export const FollowUpOpenSmall: Story = { + args: { + small: true, + defaultPanelStage: 'open', + }, +} From 55b8c5fcde73dd36dd74a63f702af9e3ceaac3b1 Mon Sep 17 00:00:00 2001 From: mariana-caetano <67270558+mariana-caetano@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:12:23 -0300 Subject: [PATCH 5/5] feat(typings): add new feedback section follow-up message types --- src/utils/typings/messages-types.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/typings/messages-types.ts b/src/utils/typings/messages-types.ts index 8548d80..69c8822 100644 --- a/src/utils/typings/messages-types.ts +++ b/src/utils/typings/messages-types.ts @@ -4,6 +4,12 @@ export type MessagesType = { 'feedback_section.positive': string 'feedback_section.negative': string 'feedback_section.edit': string + 'feedback_section.followup_placeholder': string + 'feedback_section.followup_email_placeholder': string + 'feedback_section.followup_send': string + 'feedback_section.followup_skip': string + 'feedback_section.followup_success': string + 'feedback_section.followup_error': string 'feedback_modal.button': string 'feedback_modal.title': string 'feedback_modal.description': string