Skip to content

Refreshed Portal gift tier picker and card design#27705

Merged
minimaluminium merged 3 commits intomainfrom
gift-subs-design-iteration-2
May 6, 2026
Merged

Refreshed Portal gift tier picker and card design#27705
minimaluminium merged 3 commits intomainfrom
gift-subs-design-iteration-2

Conversation

@minimaluminium
Copy link
Copy Markdown
Member

ref https://linear.app/ghost/issue/BER-3600/design-iteration

  • Redesigned the gift selection screen with an inline accordion tier picker.
  • Refreshed the gift card and right-side panel design across the selection, confirmation, redemption, and magic link pages.

Tier rows on the gift selection screen now expand inline to reveal each
tier's benefits, and the CTA sticks to the bottom with a fade gradient.
Refreshes the gift card preview on the right with a brand-coloured panel,
ID-badge style card, and lighter close icon, details toggle and benefit
text against the brand background.
…k pages

The card on the right side of the gift confirmation, redemption, and
magic-link-in-gift-mode pages now uses the brand-coloured panel and
ID-badge card from the selection page, drops the ribbon and bow ornament,
and adds a "membership" suffix to the tier name.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 82a003a8-f641-47c1-b165-e770352c14e8

📥 Commits

Reviewing files that changed from the base of the PR and between 4cdf635 and 20ca032.

📒 Files selected for processing (1)
  • apps/portal/package.json
✅ Files skipped from review due to trivial changes (1)
  • apps/portal/package.json

Walkthrough

The portal's gift UX was reworked across four pages. GiftPage received a major redesign: new CSS, an accordion-based tier selector, left/right column layout, centering via useLayoutEffect, tiltable right-card visuals, and decorative orb/ribbon/bow assets. GiftRedemptionPage, GiftSuccessPage, and MagicLinkPage had their right-side panels reorganized into a card-stack wrapper, simplified inner card structure, updated tier labeling (appending "membership"), and an optional collapsible benefits/details section. Package version for the portal was bumped from 2.68.28 to 2.68.29.

Possibly related PRs

  • TryGhost/Ghost#27668: Touches the same gift flow files and UI—reworking right-hand gift card rendering, tilt behavior, and collapsible benefits across gift pages.
  • TryGhost/Ghost#27613: Directly modifies apps/portal/src/components/pages/gift-redemption-page.js to restructure the right-side panel DOM and CSS layout.
  • TryGhost/Ghost#27610: Modifies the gift page UI/CSS and GiftPageStyles, impacting header/subtitle and visual layout changes.

Suggested labels

preview

Suggested reviewers

  • sagzy
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: redesigning gift tier selection UI and refreshing card design across gift pages.
Description check ✅ Passed The description clearly relates to the changeset, referencing the design iteration task and describing the key updates: accordion tier picker and gift card design refresh.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gift-subs-design-iteration-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Changelog for v2.68.28 -> 2.68.29:
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
apps/portal/src/components/pages/magic-link-page.js (2)

344-361: ⚡ Quick win

Cursor-driven card tilt is missing on this page.

gift-page.js, gift-redemption-page.js, and gift-success-page.js all spread cardTiltProps on .gh-portal-gift-checkout-right and pass cardRef to .gh-portal-gift-checkout-card. Here neither is wired (no useCardTilt import; class component can't use hooks directly), so the card never tilts to cursor movement — only the data-revealing reveal rotation fires. To restore visual parity, extract a small functional wrapper (e.g. <TiltCard>) that uses useCardTilt internally and renders its children, then use it here in place of the bare .gh-portal-gift-checkout-right + card-frame markup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/portal/src/components/pages/magic-link-page.js` around lines 344 - 361,
The right-side gift card markup in magic-link-page.js is missing the
cursor-driven tilt logic: create a small functional wrapper component (e.g.
TiltCard) that imports and calls useCardTilt(), spreads the returned
cardTiltProps onto the outer wrapper that currently has className
'gh-portal-gift-checkout-right', and forwards the cardRef to the inner element
with className 'gh-portal-gift-checkout-card' (or accept children and attach ref
there); replace the current plain markup with this <TiltCard> wrapper so the
card element receives the hook's ref and the wrapper receives the tilt props to
restore cursor tilt behavior.

358-358: 💤 Low value

Guard against a missing gift.tier.

gift?.tier?.name will render "undefined membership" if gift.tier is absent, since the optional chain falls through into the template literal. The isGiftMode gate at line 414 only checks pageData?.gift, not gift.tier. Either tighten the gate to require pageData?.gift?.tier, or coerce the fallback here (e.g. gift.tier?.name ? \${gift.tier.name} membership` : ''`).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/portal/src/components/pages/magic-link-page.js` at line 358, The
template currently renders `${gift.tier?.name} membership` which will show
"undefined membership" if gift.tier is missing; either tighten the existing
isGiftMode gate to require pageData?.gift?.tier (so rendering only occurs when a
tier exists) or change the expression in the gh-portal-gift-checkout-card-tier
div to coerce a safe fallback (e.g., render `${gift.tier?.name ?
`${gift.tier.name} membership` : ''}`) so that missing gift.tier does not
produce "undefined membership"; update either the isGiftMode check or the
rendering expression around gift.tier accordingly.
apps/portal/src/components/pages/gift-page.js (3)

572-594: 💤 Low value

Pass an explicit dependency array to useLayoutEffect.

Without a dependency array, this effect runs after every render (state updates, tier switches, hover-driven re-renders, etc.). The centeringDoneRef.current short-circuit keeps it cheap, but the intent is clearly "run once on mount." Pass [] so this matches the comment "On first paint" and avoids any chance of subtle re-execution if the early-out conditions ever change.

♻️ Proposed fix
     useLayoutEffect(() => {
         if (centeringDoneRef.current) {
             return;
         }
         ...
         centeringDoneRef.current = true;
-    });
+    }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/portal/src/components/pages/gift-page.js` around lines 572 - 594, The
useLayoutEffect that centers the inner element runs without a dependency array
so it executes on every render; update the hook call in the GiftPage component
(the useLayoutEffect using centeringDoneRef, innerRef, leftRef) to include an
explicit empty dependency array ([]) so it only runs once on mount/"first
paint"; ensure the existing early returns/refs (centeringDoneRef.current,
innerRef, leftRef) remain unchanged and, if your linter complains about
exhaustive-deps, add a concise eslint-disable-next-line comment scoped to this
line rather than changing the effect logic.

668-712: ⚖️ Poor tradeoff

Radiogroup is missing arrow-key navigation.

The role='radiogroup' / role='radio' pattern requires arrow keys to move focus between radios per the WAI-ARIA APG; right now only Tab works, and each radio is tab-stoppable, which is the inverse of the expected single-tabstop-with-roving-focus behavior. Consider either dropping the radio roles (and styling the buttons as a regular list) or wiring up onKeyDown to handle ArrowUp/Down/Left/Right and tabIndex to set the roving tabstop on the selected option.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/portal/src/components/pages/gift-page.js` around lines 668 - 712, The
radiogroup currently uses role='radiogroup' and role='radio' but lacks roving
keyboard behavior; update the buttons rendered in products.map (the radio
buttons that call setSelectedProductId and use activeProduct) to implement
roving focus: give each button a tabIndex of (isSelected ? 0 : -1), add an
onKeyDown handler that listens for ArrowUp/ArrowLeft and ArrowDown/ArrowRight to
compute the previous/next product index (using the products array and
product.id), call setSelectedProductId with the new id and move focus to the
corresponding button (use refs or querySelector by data-test-tier/product.id),
and ensure Enter/Space also activate selection; alternatively, if you don't want
ARIA radios, remove role='radiogroup' and role='radio' and keep them as regular
buttons—choose one approach and apply it to the elements inside the products.map
loop.

731-749: 💤 Low value

Inconsistent card-stack structure across the four gift pages.

The other three pages (gift-redemption-page.js, gift-success-page.js, magic-link-page.js) wrap the card in .gh-portal-gift-checkout-card-frame and set data-revealing on .gh-portal-gift-checkout-card-stack, since they all expose a "Gift details" toggle. Here the card-frame wrapper is omitted, which is technically fine because this page has no details toggle — but the divergence is easy to miss when refactoring. If it stays divergent, consider adding a brief comment so the structure isn't accidentally "fixed" later; otherwise add the wrapper for structural parity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/portal/src/components/pages/gift-page.js` around lines 731 - 749, The
card stack markup in gift-page.js diverges from the other pages by omitting the
.gh-portal-gift-checkout-card-frame wrapper and the data-revealing attribute on
.gh-portal-gift-checkout-card-stack; either restore structural parity by
wrapping the existing .gh-portal-gift-checkout-card element in a div with class
"gh-portal-gift-checkout-card-frame" and add data-revealing (e.g.
data-revealing="true" or matching the other pages) to the
.gh-portal-gift-checkout-card-stack element, or if you intentionally want this
page to differ, add a brief comment next to the
.gh-portal-gift-checkout-card-stack or card markup explaining why the frame is
omitted to prevent accidental refactors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/portal/src/components/pages/gift-page.js`:
- Around line 572-594: The useLayoutEffect that centers the inner element runs
without a dependency array so it executes on every render; update the hook call
in the GiftPage component (the useLayoutEffect using centeringDoneRef, innerRef,
leftRef) to include an explicit empty dependency array ([]) so it only runs once
on mount/"first paint"; ensure the existing early returns/refs
(centeringDoneRef.current, innerRef, leftRef) remain unchanged and, if your
linter complains about exhaustive-deps, add a concise eslint-disable-next-line
comment scoped to this line rather than changing the effect logic.
- Around line 668-712: The radiogroup currently uses role='radiogroup' and
role='radio' but lacks roving keyboard behavior; update the buttons rendered in
products.map (the radio buttons that call setSelectedProductId and use
activeProduct) to implement roving focus: give each button a tabIndex of
(isSelected ? 0 : -1), add an onKeyDown handler that listens for
ArrowUp/ArrowLeft and ArrowDown/ArrowRight to compute the previous/next product
index (using the products array and product.id), call setSelectedProductId with
the new id and move focus to the corresponding button (use refs or querySelector
by data-test-tier/product.id), and ensure Enter/Space also activate selection;
alternatively, if you don't want ARIA radios, remove role='radiogroup' and
role='radio' and keep them as regular buttons—choose one approach and apply it
to the elements inside the products.map loop.
- Around line 731-749: The card stack markup in gift-page.js diverges from the
other pages by omitting the .gh-portal-gift-checkout-card-frame wrapper and the
data-revealing attribute on .gh-portal-gift-checkout-card-stack; either restore
structural parity by wrapping the existing .gh-portal-gift-checkout-card element
in a div with class "gh-portal-gift-checkout-card-frame" and add data-revealing
(e.g. data-revealing="true" or matching the other pages) to the
.gh-portal-gift-checkout-card-stack element, or if you intentionally want this
page to differ, add a brief comment next to the
.gh-portal-gift-checkout-card-stack or card markup explaining why the frame is
omitted to prevent accidental refactors.

In `@apps/portal/src/components/pages/magic-link-page.js`:
- Around line 344-361: The right-side gift card markup in magic-link-page.js is
missing the cursor-driven tilt logic: create a small functional wrapper
component (e.g. TiltCard) that imports and calls useCardTilt(), spreads the
returned cardTiltProps onto the outer wrapper that currently has className
'gh-portal-gift-checkout-right', and forwards the cardRef to the inner element
with className 'gh-portal-gift-checkout-card' (or accept children and attach ref
there); replace the current plain markup with this <TiltCard> wrapper so the
card element receives the hook's ref and the wrapper receives the tilt props to
restore cursor tilt behavior.
- Line 358: The template currently renders `${gift.tier?.name} membership` which
will show "undefined membership" if gift.tier is missing; either tighten the
existing isGiftMode gate to require pageData?.gift?.tier (so rendering only
occurs when a tier exists) or change the expression in the
gh-portal-gift-checkout-card-tier div to coerce a safe fallback (e.g., render
`${gift.tier?.name ? `${gift.tier.name} membership` : ''}`) so that missing
gift.tier does not produce "undefined membership"; update either the isGiftMode
check or the rendering expression around gift.tier accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 763d54e4-3632-4f75-89ae-6bf7b9cf9442

📥 Commits

Reviewing files that changed from the base of the PR and between c8352ba and 4cdf635.

⛔ Files ignored due to path filters (1)
  • apps/portal/src/images/gift-card-orb.svg is excluded by !**/*.svg
📒 Files selected for processing (4)
  • apps/portal/src/components/pages/gift-page.js
  • apps/portal/src/components/pages/gift-redemption-page.js
  • apps/portal/src/components/pages/gift-success-page.js
  • apps/portal/src/components/pages/magic-link-page.js

@minimaluminium minimaluminium merged commit 0a696d0 into main May 6, 2026
41 checks passed
@minimaluminium minimaluminium deleted the gift-subs-design-iteration-2 branch May 6, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant