diff --git a/messages/en.json b/messages/en.json index 09e888f0..c9ee7560 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,5 +1,7 @@ { "common": { + "sealFeedbackButtonLabel": "Got feedback?", + "sealFeedbackButtonAriaLabel": "Share feedback on the Seal of Reliability (opens in a new tab)", "copyToClipboard": "Copy to clipboard", "copied": "Copied!", "name": "Name", diff --git a/messages/fr.json b/messages/fr.json index f3a76031..914395bb 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1,5 +1,7 @@ { "common": { + "sealFeedbackButtonLabel": "Des commentaires ?", + "sealFeedbackButtonAriaLabel": "Donnez votre avis sur le Sceau de fiabilité (ouvre un nouvel onglet)", "copyToClipboard": "Copier dans le presse-papiers", "copied": "Copié!", "name": "Nom", diff --git a/src/app/[locale]/seal-of-reliability/components/SealOfReliabilityDescriptionPage.tsx b/src/app/[locale]/seal-of-reliability/components/SealOfReliabilityDescriptionPage.tsx index ff904200..e8dcecc9 100644 --- a/src/app/[locale]/seal-of-reliability/components/SealOfReliabilityDescriptionPage.tsx +++ b/src/app/[locale]/seal-of-reliability/components/SealOfReliabilityDescriptionPage.tsx @@ -23,6 +23,7 @@ import { getTranslations } from 'next-intl/server'; import CardSectionTitle from '../../../components/CardSectionTitle'; import SectionContainer from '../../../components/SectionContainer'; import SealOfReliability from '../../../components/SealOfReliability'; +import SealFeedbackButton from '../../../components/SealFeedbackButton'; import { accordionStyle } from '../../../components/accordionStyle'; import { Link as LocaleLink } from '../../../../i18n/navigation'; import { @@ -49,6 +50,8 @@ export default async function SealOfReliabilityDescriptionPage(): Promise + + { return ( + + { + setIsMounted(true); + }, []); + + // Before mount the link still points at the form, just without the + // prefill, so it works for anyone who clicks during hydration. + const feedbackUrl = useMemo( + () => + isMounted ? buildSealFeedbackUrl({ name, email }) : SEAL_FEEDBACK_URL, + [isMounted, name, email], + ); + + return ( + <> + + theme.zIndex.appBar - 1, + }} + > + + + + ); +} diff --git a/src/app/screens/Feed/components/FeedReliabilityView.tsx b/src/app/screens/Feed/components/FeedReliabilityView.tsx index a5044641..686a3966 100644 --- a/src/app/screens/Feed/components/FeedReliabilityView.tsx +++ b/src/app/screens/Feed/components/FeedReliabilityView.tsx @@ -34,6 +34,7 @@ import { getCoverageWindowLength } from '../lib/continuous-coverage'; import { getLatestCoverageWindow } from '../lib/fresh-coverage'; import { displayFormattedDate } from '../../../utils/date'; import SectionContainer from '../../../components/SectionContainer'; +import SealFeedbackButton from '../../../components/SealFeedbackButton'; interface Props { feed: AllFeedType; @@ -102,6 +103,7 @@ export default async function FeedReliabilityView({ > + diff --git a/src/app/utils/seal-feedback-url.spec.ts b/src/app/utils/seal-feedback-url.spec.ts new file mode 100644 index 00000000..be2737c0 --- /dev/null +++ b/src/app/utils/seal-feedback-url.spec.ts @@ -0,0 +1,48 @@ +import { buildSealFeedbackUrl, SEAL_FEEDBACK_URL } from './seal-feedback-url'; + +describe('buildSealFeedbackUrl', () => { + it('returns the bare form URL when nothing is known about the user', () => { + const url = buildSealFeedbackUrl(); + + expect(url).toBe('https://share.mobilitydata.org/sealfeedback'); + expect(url).not.toContain('usp=pp_url'); + expect(url).not.toContain('entry.'); + }); + + it('exposes the same bare URL the server renders before hydration', () => { + expect(SEAL_FEEDBACK_URL).toBe(buildSealFeedbackUrl()); + }); + + it('prefills both name and email when both are available', () => { + const url = new URL( + buildSealFeedbackUrl({ + name: 'Ada Lovelace', + email: 'ada@example.org', + }), + ); + + expect(url.searchParams.get('usp')).toBe('pp_url'); + expect(url.searchParams.get('entry.1481076408')).toBe('Ada Lovelace'); + expect(url.searchParams.get('entry.1610387831')).toBe('ada@example.org'); + }); + + it('omits the fields it does not have', () => { + const url = new URL(buildSealFeedbackUrl({ email: 'ada@example.org' })); + + expect(url.searchParams.has('entry.1481076408')).toBe(false); + expect(url.searchParams.get('entry.1610387831')).toBe('ada@example.org'); + }); + + it('ignores blank and null values rather than prefilling empty fields', () => { + const url = buildSealFeedbackUrl({ name: ' ', email: null }); + + expect(url).not.toContain('usp=pp_url'); + expect(url).not.toContain('entry.'); + }); + + it('trims surrounding whitespace before prefilling', () => { + const url = new URL(buildSealFeedbackUrl({ name: ' Ada Lovelace ' })); + + expect(url.searchParams.get('entry.1481076408')).toBe('Ada Lovelace'); + }); +}); diff --git a/src/app/utils/seal-feedback-url.ts b/src/app/utils/seal-feedback-url.ts new file mode 100644 index 00000000..c56fce7b --- /dev/null +++ b/src/app/utils/seal-feedback-url.ts @@ -0,0 +1,51 @@ +/** + * It's a redirect to a google form + */ +const SEAL_FEEDBACK_FORM_URL = 'https://share.mobilitydata.org/sealfeedback'; + +/** + * Field ids taken from the form's own "Get pre-filled link" output. They are + * stable for the life of a question, but deleting and re-adding the "Name" or + * "Email" question in the form mints a new id and silently stops the prefill - + * regenerate them from the form if prefilled values stop showing up. + */ +const NAME_ENTRY_ID = 'entry.1481076408'; +const EMAIL_ENTRY_ID = 'entry.1610387831'; + +/** The form with nothing prefilled - what the server renders. */ +export const SEAL_FEEDBACK_URL = SEAL_FEEDBACK_FORM_URL; + +export interface SealFeedbackPrefill { + name?: string | null; + email?: string | null; +} + +/** + * Builds the feedback form URL, prefilling the respondent's name and email + * when the app knows them. Blank or missing values are left out entirely so + * the form renders an empty field rather than an empty prefill. + */ +export function buildSealFeedbackUrl( + prefill: SealFeedbackPrefill = {}, +): string { + const url = new URL(SEAL_FEEDBACK_FORM_URL); + const name = prefill.name?.trim() ?? ''; + const email = prefill.email?.trim() ?? ''; + + if (name === '' && email === '') { + return url.toString(); + } + + // Google's own marker for a prefilled link; without it the form ignores the + // entry parameters. + url.searchParams.set('usp', 'pp_url'); + + if (name !== '') { + url.searchParams.set(NAME_ENTRY_ID, name); + } + if (email !== '') { + url.searchParams.set(EMAIL_ENTRY_ID, email); + } + + return url.toString(); +}