Skip to content

Commit 966c1ba

Browse files
Cleaned up gift subscription screens (TryGhost#27825)
ref https://linear.app/ghost/issue/BER-3605/design-cleanup Follow-up cleanup from the gift subscription redesign (TryGhost#27668). - **BER-3607** — gift selection and success screens now use the shared `getGiftDurationLabel` util instead of local copies. - **BER-3606** — extracted `<GiftCard>` and `<GiftDetailsToggle>` shared components, used across all four gift screens (selection, success, redemption, magic-link in gift mode).
1 parent 39ee9fb commit 966c1ba

7 files changed

Lines changed: 177 additions & 262 deletions

File tree

apps/portal/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tryghost/portal",
3-
"version": "2.68.37",
3+
"version": "2.68.38",
44
"license": "MIT",
55
"repository": "https://github.com/TryGhost/Ghost",
66
"author": "Ghost Foundation",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// TODO: wrap strings with t() once copy is finalised
2+
/* eslint-disable i18next/no-literal-string */
3+
4+
const GiftCard = ({cardRef, duration, tierName, name, giftValue, siteIcon, siteTitle}) => {
5+
const hasMeta = duration && tierName;
6+
const hasDetails = name || giftValue;
7+
8+
return (
9+
<div className='gh-portal-gift-checkout-card-frame'>
10+
<div ref={cardRef} className='gh-portal-gift-checkout-card'>
11+
<div className='gh-portal-gift-checkout-card-notch' aria-hidden='true' />
12+
{hasMeta && (
13+
<div className='gh-portal-gift-checkout-card-meta'>
14+
<div className='gh-portal-gift-checkout-card-duration'>{duration}</div>
15+
<div className='gh-portal-gift-checkout-card-tier'>{`${tierName} membership`}</div>
16+
</div>
17+
)}
18+
{hasDetails && (
19+
<div className='gh-portal-gift-checkout-card-details'>
20+
{name && (
21+
<div className='gh-portal-gift-checkout-card-detail'>
22+
<div className='gh-portal-gift-checkout-card-detail-label'>Name</div>
23+
<div className='gh-portal-gift-checkout-card-detail-value'>{name}</div>
24+
</div>
25+
)}
26+
{giftValue && (
27+
<div className='gh-portal-gift-checkout-card-detail'>
28+
<div className='gh-portal-gift-checkout-card-detail-label'>Gift value</div>
29+
<div className='gh-portal-gift-checkout-card-detail-value'>{giftValue}</div>
30+
</div>
31+
)}
32+
</div>
33+
)}
34+
<div className='gh-portal-gift-checkout-card-site'>
35+
{siteIcon && (
36+
<img className='gh-portal-gift-checkout-card-site-icon' src={siteIcon} alt='' />
37+
)}
38+
<span className='gh-portal-gift-checkout-card-site-name'>{siteTitle}</span>
39+
</div>
40+
</div>
41+
</div>
42+
);
43+
};
44+
45+
export default GiftCard;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
2+
3+
// TODO: wrap strings with t() once copy is finalised
4+
/* eslint-disable i18next/no-literal-string */
5+
6+
const ChevronIcon = () => (
7+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
8+
<polyline points="6 9 12 15 18 9"/>
9+
</svg>
10+
);
11+
12+
const GiftDetailsToggle = ({description, benefits, showDetails, onToggle}) => {
13+
const visibleBenefits = (benefits || [])
14+
.map((benefit, index) => {
15+
const benefitName = typeof benefit === 'string' ? benefit : benefit?.name;
16+
const benefitKey = typeof benefit === 'string' ? benefit : benefit?.id || `gift-benefit-${index}`;
17+
18+
if (!benefitName) {
19+
return null;
20+
}
21+
22+
return (
23+
<div className='gh-portal-gift-checkout-benefit' key={benefitKey}>
24+
<CheckmarkIcon aria-hidden='true' focusable='false' />
25+
<span>{benefitName}</span>
26+
</div>
27+
);
28+
})
29+
.filter(Boolean);
30+
31+
if (!description && visibleBenefits.length === 0) {
32+
return null;
33+
}
34+
35+
return (
36+
<>
37+
<div
38+
className='gh-portal-gift-checkout-details'
39+
data-open={showDetails}
40+
aria-hidden={!showDetails}
41+
>
42+
<div className='gh-portal-gift-checkout-details-inner'>
43+
{description && (
44+
<p className='gh-portal-gift-checkout-details-description'>{description}</p>
45+
)}
46+
{visibleBenefits.length > 0 && (
47+
<div className='gh-portal-gift-checkout-benefits'>
48+
{visibleBenefits}
49+
</div>
50+
)}
51+
</div>
52+
</div>
53+
<button
54+
type='button'
55+
className={'gh-portal-gift-checkout-details-toggle' + (showDetails ? ' is-open' : '')}
56+
onClick={onToggle}
57+
aria-expanded={showDetails}
58+
>
59+
{showDetails ? 'Hide details' : 'Gift details'}
60+
<ChevronIcon />
61+
</button>
62+
</>
63+
);
64+
};
65+
66+
export default GiftDetailsToggle;

apps/portal/src/components/pages/gift-page.js

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {useContext, useLayoutEffect, useRef, useState} from 'react';
22
import AppContext from '../../app-context';
33
import CloseButton from '../common/close-button';
44
import ActionButton from '../common/action-button';
5+
import GiftCard from '../common/gift-card';
56
import LoadingPage from './loading-page';
67
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
78
import giftCardNoiseUrl from '../../images/gift-card-noise.webp';
89
import giftCardOrbUrl from '../../images/gift-card-orb.webp';
910
import {getAvailableProducts, getCurrencySymbol, formatNumber, getStripeAmount, isCookiesDisabled, getActiveInterval} from '../../utils/helpers';
11+
import {getGiftDurationLabel} from '../../utils/gift-redemption-notification';
1012
import useCardTilt from '../../utils/use-card-tilt';
1113

1214
// TODO: wrap strings with t() once copy is finalised
@@ -651,10 +653,6 @@ function getTierPriceLabel(product, selectedInterval) {
651653
return formatGiftValue(activePrice);
652654
}
653655

654-
function getDurationLabel(selectedInterval) {
655-
return selectedInterval === 'month' ? '1 month' : '1 year';
656-
}
657-
658656
const GiftPage = () => {
659657
const {site, brandColor, action, doAction} = useContext(AppContext);
660658
const [selectedInterval, setSelectedInterval] = useState(null);
@@ -819,7 +817,7 @@ const GiftPage = () => {
819817
const key = benefit?.id || `benefit-${idx}`;
820818
return (
821819
<div className='gh-portal-gift-checkout-benefit' key={key}>
822-
<CheckmarkIcon alt='' />
820+
<CheckmarkIcon aria-hidden='true' focusable='false' />
823821
<span>{benefit.name}</span>
824822
</div>
825823
);
@@ -852,26 +850,14 @@ const GiftPage = () => {
852850
<div className='gh-portal-gift-checkout-right' {...cardTiltProps}>
853851
<div className='gh-portal-gift-checkout-right-panel'>
854852
<div className='gh-portal-gift-checkout-card-stack'>
855-
<div ref={cardRef} className='gh-portal-gift-checkout-card'>
856-
<div className='gh-portal-gift-checkout-card-notch' aria-hidden='true' />
857-
<div className='gh-portal-gift-checkout-card-meta'>
858-
<div className='gh-portal-gift-checkout-card-duration'>{getDurationLabel(activeInterval)}</div>
859-
<div className='gh-portal-gift-checkout-card-tier'>{`${activeProduct.name} membership`}</div>
860-
</div>
861-
<div className='gh-portal-gift-checkout-card-details'>
862-
<div className='gh-portal-gift-checkout-card-detail'>
863-
<div className='gh-portal-gift-checkout-card-detail-label'>Gift value</div>
864-
<div className='gh-portal-gift-checkout-card-detail-value'>{getTierPriceLabel(activeProduct, activeInterval)}</div>
865-
</div>
866-
</div>
867-
<div className='gh-portal-gift-checkout-card-site'>
868-
{siteIcon && (
869-
<img className='gh-portal-gift-checkout-card-site-icon' src={siteIcon} alt='' />
870-
)}
871-
<span className='gh-portal-gift-checkout-card-site-name'>{siteTitle}</span>
872-
</div>
873-
</div>
874-
853+
<GiftCard
854+
cardRef={cardRef}
855+
duration={getGiftDurationLabel({cadence: activeInterval, duration: 1})}
856+
tierName={activeProduct.name}
857+
giftValue={getTierPriceLabel(activeProduct, activeInterval)}
858+
siteIcon={siteIcon}
859+
siteTitle={siteTitle}
860+
/>
875861
</div>
876862
</div>
877863
</div>

apps/portal/src/components/pages/gift-redemption-page.js

Lines changed: 18 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import {useContext, useEffect, useState} from 'react';
22
import AppContext from '../../app-context';
33
import ActionButton from '../common/action-button';
44
import CloseButton from '../common/close-button';
5+
import GiftCard from '../common/gift-card';
6+
import GiftDetailsToggle from '../common/gift-details-toggle';
57
import InputForm from '../common/input-form';
68
import {ValidateInputForm} from '../../utils/form';
7-
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
89
import {getGiftDurationLabel, getGiftRedemptionErrorMessage} from '../../utils/gift-redemption-notification';
910
import {t} from '../../utils/i18n';
1011
import {hasGiftSubscriptions, removePortalLinkFromUrl} from '../../utils/helpers';
@@ -21,12 +22,6 @@ export const GiftRedemptionStyles = `
2122
}
2223
`;
2324

24-
const ChevronIcon = () => (
25-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
26-
<polyline points="6 9 12 15 18 9"/>
27-
</svg>
28-
);
29-
3025
// TODO: Add translation strings once copy has been finalised
3126
const GiftRedemptionPage = () => {
3227
const {action, brandColor, doAction, member, pageData, site} = useContext(AppContext);
@@ -200,81 +195,22 @@ const GiftRedemptionPage = () => {
200195
<div className='gh-portal-gift-checkout-right' {...cardTiltProps}>
201196
<div className='gh-portal-gift-checkout-right-panel'>
202197
<div className='gh-portal-gift-checkout-card-stack' data-revealing={showDetails}>
203-
<div className='gh-portal-gift-checkout-card-frame'>
204-
<div ref={cardRef} className='gh-portal-gift-checkout-card'>
205-
<div className='gh-portal-gift-checkout-card-notch' aria-hidden='true' />
206-
<div className='gh-portal-gift-checkout-card-meta'>
207-
<div className='gh-portal-gift-checkout-card-duration'>{getGiftDurationLabel(gift)}</div>
208-
{/* eslint-disable-next-line i18next/no-literal-string -- copy not yet finalised */}
209-
<div className='gh-portal-gift-checkout-card-tier'>{`${gift.tier.name} membership`}</div>
210-
</div>
211-
<div className='gh-portal-gift-checkout-card-details'>
212-
{name.trim() && (
213-
<div className='gh-portal-gift-checkout-card-detail'>
214-
{/* eslint-disable-next-line i18next/no-literal-string -- copy not yet finalised */}
215-
<div className='gh-portal-gift-checkout-card-detail-label'>Name</div>
216-
<div className='gh-portal-gift-checkout-card-detail-value'>{name.trim()}</div>
217-
</div>
218-
)}
219-
<div className='gh-portal-gift-checkout-card-detail'>
220-
{/* eslint-disable-next-line i18next/no-literal-string -- copy not yet finalised */}
221-
<div className='gh-portal-gift-checkout-card-detail-label'>Gift value</div>
222-
<div className='gh-portal-gift-checkout-card-detail-value'>{formatGiftValue(gift)}</div>
223-
</div>
224-
</div>
225-
<div className='gh-portal-gift-checkout-card-site'>
226-
{siteIcon && (
227-
<img className='gh-portal-gift-checkout-card-site-icon' src={siteIcon} alt='' />
228-
)}
229-
<span className='gh-portal-gift-checkout-card-site-name'>{siteTitle}</span>
230-
</div>
231-
</div>
232-
</div>
233-
234-
{(tierDescription || benefits.length > 0) && (
235-
<>
236-
<div
237-
className='gh-portal-gift-checkout-details'
238-
data-open={showDetails}
239-
aria-hidden={!showDetails}
240-
>
241-
<div className='gh-portal-gift-checkout-details-inner'>
242-
{tierDescription && (
243-
<p className='gh-portal-gift-checkout-details-description'>{tierDescription}</p>
244-
)}
245-
{benefits.length > 0 && (
246-
<div className='gh-portal-gift-checkout-benefits'>
247-
{benefits.map((benefit, index) => {
248-
const benefitName = typeof benefit === 'string' ? benefit : benefit?.name;
249-
const benefitKey = typeof benefit === 'string' ? benefit : benefit?.id || `gift-benefit-${index}`;
250-
251-
if (!benefitName) {
252-
return null;
253-
}
254-
255-
return (
256-
<div className='gh-portal-gift-checkout-benefit' key={benefitKey}>
257-
<CheckmarkIcon alt='' />
258-
<span>{benefitName}</span>
259-
</div>
260-
);
261-
})}
262-
</div>
263-
)}
264-
</div>
265-
</div>
266-
<button
267-
type='button'
268-
className={'gh-portal-gift-checkout-details-toggle' + (showDetails ? ' is-open' : '')}
269-
onClick={() => setShowDetails(s => !s)}
270-
aria-expanded={showDetails}
271-
>
272-
{/* eslint-disable-next-line i18next/no-literal-string -- copy not yet finalised */}
273-
{showDetails ? 'Hide details' : 'Gift details'}
274-
<ChevronIcon />
275-
</button>
276-
</>
277-
)}
198+
<GiftCard
199+
cardRef={cardRef}
200+
duration={getGiftDurationLabel(gift)}
201+
tierName={gift.tier.name}
202+
name={name.trim() || null}
203+
giftValue={formatGiftValue(gift)}
204+
siteIcon={siteIcon}
205+
siteTitle={siteTitle}
206+
/>
207+
208+
<GiftDetailsToggle
209+
description={tierDescription}
210+
benefits={benefits}
211+
showDetails={showDetails}
212+
onToggle={() => setShowDetails(s => !s)}
213+
/>
278214
</div>
279215
</div>
280216
</div>

0 commit comments

Comments
 (0)