Skip to content

[docs] Add analytics and close banner btn#4005

Merged
m-bert merged 1 commit intosoftware-mansion:mainfrom
p-malecki:@p-malecki/add-banner-analytics
Feb 27, 2026
Merged

[docs] Add analytics and close banner btn#4005
m-bert merged 1 commit intosoftware-mansion:mainfrom
p-malecki:@p-malecki/add-banner-analytics

Conversation

@p-malecki
Copy link
Copy Markdown
Contributor

Description

Adds a dedicated GTM container for banner tracking and introduces a close option in docs with localStorage persistence. These changes will be replaced by the upcoming banner system in the future.

Copilot AI review requested due to automatic review settings February 27, 2026 14:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds banner analytics and a dismissible promo banner behavior to the Gesture Handler docs site, with persistence via localStorage for non-landing pages.

Changes:

  • Add localStorage-backed close behavior for the top promo banner on non-landing pages.
  • Inject an additional GTM container intended for banner tracking.
  • Add close button styling for the promo rotator.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
packages/docs-gesture-handler/src/theme/Navbar/index.js Adds page-type detection and localStorage persistence to conditionally render the promo rotator with a close handler.
packages/docs-gesture-handler/src/components/TopPromoRotator/index.tsx Adds onClose support, exports PROMO_VERSION, and injects a dedicated GTM container script.
packages/docs-gesture-handler/src/components/TopPromoRotator/styles.module.css Styles the new close button.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +66 to +90
useEffect(() => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}

const existingScript = document.querySelector<HTMLScriptElement>(
'script[src*="www.googletagmanager.com/gtm.js?id=GTM-WV2G3SQL"]'
);

if (existingScript) return;

(function (w: Window, d: Document, s: string, l: string, i: string) {
w.dataLayer = w.dataLayer || [];
w.dataLayer.push({
'gtm.start': new Date().getTime(),
event: 'gtm.js',
});
const f = d.getElementsByTagName(s)[0] as HTMLScriptElement;
const j = d.createElement(s) as HTMLScriptElement;
const dl = l !== 'dataLayer' ? `&l=${l}` : '';
j.async = true;
j.src = `https://www.googletagmanager.com/gtm.js?id=${i}${dl}`;
f.parentNode?.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-WV2G3SQL');
}, []);
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component injects a second GTM container (GTM-WV2G3SQL) at runtime, but the site already loads GTM via @docusaurus/plugin-google-tag-manager (GTM-PHF2NKVT in docusaurus.config.js). Loading two containers on the same dataLayer can lead to duplicated tracking/perf overhead and makes analytics configuration harder to reason about. Consider moving this to the Docusaurus config (or a custom plugin) and/or using a distinct dataLayer name for the dedicated container if it must coexist.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that both work fine, right @p-malecki?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it won't be any problem.

Comment on lines +131 to +139
{onClose && (
<button
type="button"
className={styles.closeButton}
aria-label="Close promotion banner"
onClick={onClose}>
×
</button>
)}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close button is absolutely positioned over the full-width <a> banner without reserving space, so at narrower widths the button can overlap banner text/CTA. Consider reserving right-side padding/margin when onClose is present (e.g., a wrapper modifier class) so content layout doesn’t collide with the close affordance.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +91
padding: 4px;
line-height: 1;
font-size: 16px;
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.closeButton’s hit area is quite small (roughly 24×24px with current padding/font-size), which is below common touch target guidelines and can be hard to tap on mobile. Consider increasing padding/size (and optionally adding :focus-visible styling) to improve usability and accessibility.

Suggested change
padding: 4px;
line-height: 1;
font-size: 16px;
}
padding: 8px;
min-width: 32px;
min-height: 32px;
line-height: 1;
font-size: 16px;
}
.closeButton:focus-visible {
outline: 2px solid #001a72;
outline-offset: 2px;
}

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +31
const [showPromo, setShowPromo] = React.useState(true);

React.useEffect(() => {
if (isLanding || typeof globalThis === 'undefined') {
return;
}

try {
const raw = globalThis.localStorage?.getItem('topPromoState');
const state = raw ? JSON.parse(raw) : null;
if (state?.v === PROMO_VERSION && state?.hidden) {
setShowPromo(false);
}
} catch (_) {
// ignore
}
}, [isLanding]);

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showPromo is initialized to true and only updated after mount in an effect. If the user previously dismissed the promo, this will still render the banner briefly (UI flash) before setShowPromo(false) runs. Consider initializing state lazily from localStorage (when not landing and in the browser) so the first render matches the persisted hidden state.

Suggested change
const [showPromo, setShowPromo] = React.useState(true);
React.useEffect(() => {
if (isLanding || typeof globalThis === 'undefined') {
return;
}
try {
const raw = globalThis.localStorage?.getItem('topPromoState');
const state = raw ? JSON.parse(raw) : null;
if (state?.v === PROMO_VERSION && state?.hidden) {
setShowPromo(false);
}
} catch (_) {
// ignore
}
}, [isLanding]);
const [showPromo, setShowPromo] = React.useState(() => {
// On the landing page, `showPromo` is not used, so default to true.
if (isLanding) {
return true;
}
// Only attempt to read from localStorage in the browser.
if (typeof window === 'undefined' || !window.localStorage) {
return true;
}
try {
const raw = window.localStorage.getItem('topPromoState');
const state = raw ? JSON.parse(raw) : null;
if (state?.v === PROMO_VERSION && state?.hidden) {
return false;
}
} catch {
// ignore and fall through to default
}
return true;
});

Copilot uses AI. Check for mistakes.
@m-bert m-bert merged commit 13c05f2 into software-mansion:main Feb 27, 2026
5 of 6 checks passed
@m-bert m-bert added the Documentation Documentation change/enhancement label Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Documentation change/enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants