Repository navigation
fix(marketing): cookie banner no longer covers the hero download button #9218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,16 +2,83 @@ | |
|
|
||
| import { Trans } from "@lingui/react/macro"; | ||
| import { Button } from "@superset/ui/button"; | ||
| import { cn } from "@superset/ui/utils"; | ||
| import { AnimatePresence, m } from "framer-motion"; | ||
| import Link from "next/link"; | ||
| import { useEffect, useState } from "react"; | ||
| import { useEffect, useLayoutEffect, useRef, useState } from "react"; | ||
|
|
||
| import { declineAnalytics } from "@/lib/analytics/decline"; | ||
| import { withPosthog } from "@/lib/analytics/lazy"; | ||
| import { ANALYTICS_CONSENT_KEY } from "@/lib/constants"; | ||
|
|
||
| /** Elements the banner must never cover, such as the hero download buttons. */ | ||
| export const KEEP_CLEAR_ATTRIBUTE = "data-cookie-banner-keep-clear"; | ||
|
|
||
| // Tried in order; the first one that leaves every keep-clear element | ||
| // uncovered wins. A browser window is often smaller than the screen, so no | ||
| // single fixed position is safe: on a 1000x560 Safari window a bottom bar | ||
| // covers the hero CTA, and on a phone a corner card does. | ||
| const PLACEMENTS = [ | ||
| { | ||
| name: "corner-left", | ||
| container: "bottom-4 left-4 w-72 flex-col items-stretch", | ||
| compact: false, | ||
| }, | ||
| { | ||
| name: "corner-right", | ||
| container: "bottom-4 right-4 w-72 flex-col items-stretch", | ||
| compact: false, | ||
| }, | ||
| { | ||
| name: "bar", | ||
| container: "inset-x-4 bottom-4 flex-col sm:flex-row sm:items-center", | ||
| compact: false, | ||
| }, | ||
| { | ||
| name: "compact-right", | ||
| container: "bottom-2 right-2 flex-row items-center", | ||
| compact: true, | ||
| }, | ||
| { | ||
| name: "compact-left", | ||
| container: "bottom-2 left-2 flex-row items-center", | ||
| compact: true, | ||
| }, | ||
| ] as const; | ||
|
|
||
| function overlaps(a: DOMRect, b: DOMRect): boolean { | ||
| return ( | ||
| a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom | ||
| ); | ||
| } | ||
|
|
||
| function coversKeepClear(banner: HTMLElement): boolean { | ||
| // Measure where the banner sits once its entrance animation ends. | ||
| const rect = banner.getBoundingClientRect(); | ||
| const offsetY = Number.parseFloat( | ||
| getComputedStyle(banner).transform.split(",")[5] ?? "0", | ||
| ); | ||
| const settled = new DOMRect( | ||
| rect.x, | ||
| rect.y - (Number.isFinite(offsetY) ? offsetY : 0), | ||
| rect.width, | ||
| rect.height, | ||
| ); | ||
| for (const element of document.querySelectorAll( | ||
| `[${KEEP_CLEAR_ATTRIBUTE}]`, | ||
| )) { | ||
| const target = element.getBoundingClientRect(); | ||
| if (target.width === 0 || target.height === 0) continue; | ||
| if (overlaps(settled, target)) return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| export function CookieConsent() { | ||
| const [showBanner, setShowBanner] = useState(false); | ||
| const [placementIndex, setPlacementIndex] = useState(0); | ||
| const [viewport, setViewport] = useState<string | null>(null); | ||
| const bannerRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| useEffect(() => { | ||
| const consent = localStorage.getItem(ANALYTICS_CONSENT_KEY); | ||
|
|
@@ -20,6 +87,30 @@ export function CookieConsent() { | |
| } | ||
| }, []); | ||
|
|
||
| // Start over from the preferred placement whenever the window changes size. | ||
| useEffect(() => { | ||
| if (!showBanner) return; | ||
| const onResize = () => | ||
| setViewport(`${window.innerWidth}x${window.innerHeight}`); | ||
| onResize(); | ||
| window.addEventListener("resize", onResize); | ||
| return () => window.removeEventListener("resize", onResize); | ||
| }, [showBanner]); | ||
|
|
||
| // biome-ignore lint/correctness/useExhaustiveDependencies: viewport is the trigger | ||
| useLayoutEffect(() => { | ||
| setPlacementIndex(0); | ||
| }, [viewport]); | ||
|
|
||
| // biome-ignore lint/correctness/useExhaustiveDependencies: re-measure after a resize even when the placement stays the same | ||
| useLayoutEffect(() => { | ||
| const banner = bannerRef.current; | ||
| if (!showBanner || !banner) return; | ||
| if (placementIndex < PLACEMENTS.length - 1 && coversKeepClear(banner)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: The final Prompt for AI agents |
||
| setPlacementIndex(placementIndex + 1); | ||
| } | ||
| }, [showBanner, placementIndex, viewport]); | ||
|
Comment on lines
+105
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recheck placement when marked actions move. If a visitor scrolls while the banner is open, a marked action can move behind the fixed banner without changing any dependency of this effect. Client-side navigation can also replace marked actions while the banner remains mounted in the layout. Recheck overlap when the marked elements move or change; otherwise, a previously safe placement can cover a CTA. (raw.githubusercontent.com)
|
||
|
|
||
| const handleAccept = () => { | ||
| localStorage.setItem(ANALYTICS_CONSENT_KEY, "accepted"); | ||
| setShowBanner(false); | ||
|
|
@@ -32,36 +123,59 @@ export function CookieConsent() { | |
| setShowBanner(false); | ||
| }; | ||
|
|
||
| const placement = PLACEMENTS[placementIndex] ?? PLACEMENTS[0]; | ||
|
|
||
| return ( | ||
| <AnimatePresence> | ||
| {showBanner && ( | ||
| <m.div | ||
| ref={bannerRef} | ||
| data-placement={placement.name} | ||
| initial={{ y: 20, opacity: 0 }} | ||
| animate={{ y: 0, opacity: 1 }} | ||
| exit={{ y: 20, opacity: 0 }} | ||
| transition={{ type: "spring", damping: 25, stiffness: 300 }} | ||
| className="fixed bottom-4 left-4 z-50 max-w-xs rounded-lg border border-border bg-card p-4 shadow-lg" | ||
| className={cn( | ||
| "fixed z-50 flex rounded-lg border border-border bg-card shadow-lg", | ||
| placement.compact ? "gap-2 p-2" : "gap-3 p-3 sm:p-4", | ||
| placement.container, | ||
| )} | ||
| > | ||
| <p className="text-sm text-muted-foreground"> | ||
| <Trans> | ||
| We only collect analytics cookies so we can improve your | ||
| experience. | ||
| </Trans> | ||
| </p> | ||
| <div className="mt-3 flex flex-col items-start gap-2"> | ||
| <Button variant="link" asChild className="px-0"> | ||
| <Link href="/privacy"> | ||
| <Trans>Privacy policy</Trans> | ||
| <p | ||
| className={cn( | ||
| "text-muted-foreground", | ||
| placement.compact ? "text-xs" : "text-sm sm:flex-1", | ||
| )} | ||
| > | ||
| {placement.compact ? ( | ||
| <Link | ||
| href="/privacy" | ||
| className="font-medium text-foreground underline-offset-4 hover:underline" | ||
| > | ||
| <Trans>Analytics cookies</Trans> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Compact mode drops the existing explanation and introduces a new Prompt for AI agents |
||
| </Link> | ||
| ) : ( | ||
| <> | ||
| <Trans> | ||
| We only collect analytics cookies so we can improve your | ||
| experience. | ||
| </Trans>{" "} | ||
| <Link | ||
| href="/privacy" | ||
| className="font-medium text-foreground underline-offset-4 hover:underline" | ||
| > | ||
| <Trans>Privacy policy</Trans> | ||
| </Link> | ||
| </> | ||
| )} | ||
| </p> | ||
| <div className="grid shrink-0 grid-cols-2 gap-2"> | ||
| <Button variant="outline" size="sm" onClick={handleOptOut}> | ||
| <Trans>Opt-out</Trans> | ||
| </Button> | ||
| <Button variant="outline" size="sm" onClick={handleAccept}> | ||
| <Trans>Accept</Trans> | ||
| </Button> | ||
| <div className="grid w-full grid-cols-2 gap-2"> | ||
| <Button variant="outline" onClick={handleOptOut}> | ||
| <Trans>Opt-out</Trans> | ||
| </Button> | ||
| <Button variant="outline" onClick={handleAccept}> | ||
| <Trans>Accept</Trans> | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </m.div> | ||
| )} | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2599,6 +2599,9 @@ msgstr "Сталася невідома помилка" | |||||
| msgid "An update is ready to install." | ||||||
| msgstr "Оновлення готове до встановлення." | ||||||
|
|
||||||
| msgid "Analytics cookies" | ||||||
| msgstr "Аналітичні cookie" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Use the catalog’s existing Ukrainian terminology here: Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| msgid "Android is on the way. Be first to know when it ships." | ||||||
| msgstr "Версія для Android уже на підході. Дізнайтеся про вихід першими." | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mark the actions, not the full-width row.
If this row reaches the banner’s height, its flex container can extend across empty space to the right of the buttons. The overlap check then rejects
compact-righteven when it clears both actions. Selection can continue tocompact-left, which may cover the left-aligned download button. Mark each action’s bounds instead of the row’s bounds. (raw.githubusercontent.com)