mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 03:14:03 +03:00
Updated Tips & Donations settings design (#20649)
REF MOM-315 - Changed to column layout - Fixed broken currency dropdown - Included a link to Stripe terms & conditions - Renamed from "Tips or donations" to "Tips & donations"
This commit is contained in:
parent
b3b9c89544
commit
806fce191d
@ -186,7 +186,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<NavItem icon='heart' keywords={growthSearchKeywords.recommendations} navid='recommendations' title="Recommendations" onClick={handleSectionClick} />
|
<NavItem icon='heart' keywords={growthSearchKeywords.recommendations} navid='recommendations' title="Recommendations" onClick={handleSectionClick} />
|
||||||
<NavItem icon='emailfield' keywords={growthSearchKeywords.embedSignupForm} navid='embed-signup-form' title="Embeddable signup form" onClick={handleSectionClick} />
|
<NavItem icon='emailfield' keywords={growthSearchKeywords.embedSignupForm} navid='embed-signup-form' title="Embeddable signup form" onClick={handleSectionClick} />
|
||||||
{hasStripeEnabled && <NavItem icon='discount' keywords={growthSearchKeywords.offers} navid='offers' title="Offers" onClick={handleSectionClick} />}
|
{hasStripeEnabled && <NavItem icon='discount' keywords={growthSearchKeywords.offers} navid='offers' title="Offers" onClick={handleSectionClick} />}
|
||||||
{hasTipsAndDonations && <NavItem icon='piggybank' keywords={growthSearchKeywords.tips} navid='tips-or-donations' title="Tips or donations" onClick={handleSectionClick} />}
|
{hasTipsAndDonations && <NavItem icon='piggybank' keywords={growthSearchKeywords.tips} navid='tips-and-donations' title="Tips & donations" onClick={handleSectionClick} />}
|
||||||
</SettingNavSection>
|
</SettingNavSection>
|
||||||
|
|
||||||
<SettingNavSection isVisible={checkVisible(Object.values(emailSearchKeywords).flat())} title="Email newsletter">
|
<SettingNavSection isVisible={checkVisible(Object.values(emailSearchKeywords).flat())} title="Email newsletter">
|
||||||
|
@ -3,13 +3,13 @@ import Offers from './Offers';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Recommendations from './Recommendations';
|
import Recommendations from './Recommendations';
|
||||||
import SearchableSection from '../../SearchableSection';
|
import SearchableSection from '../../SearchableSection';
|
||||||
import TipsOrDonations from './TipsOrDonations';
|
import TipsAndDonations from './TipsAndDonations';
|
||||||
import useFeatureFlag from '../../../hooks/useFeatureFlag';
|
import useFeatureFlag from '../../../hooks/useFeatureFlag';
|
||||||
import {checkStripeEnabled} from '@tryghost/admin-x-framework/api/settings';
|
import {checkStripeEnabled} from '@tryghost/admin-x-framework/api/settings';
|
||||||
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||||
|
|
||||||
export const searchKeywords = {
|
export const searchKeywords = {
|
||||||
tips: ['growth', 'tip', 'donation', 'one time', 'payment'],
|
tips: ['growth', 'tips', 'donations', 'one time', 'payment'],
|
||||||
embedSignupForm: ['growth', 'embeddable signup form', 'embeddable form', 'embeddable sign up form', 'embeddable sign up'],
|
embedSignupForm: ['growth', 'embeddable signup form', 'embeddable form', 'embeddable sign up form', 'embeddable sign up'],
|
||||||
recommendations: ['growth', 'recommendations', 'recommend', 'blogroll'],
|
recommendations: ['growth', 'recommendations', 'recommend', 'blogroll'],
|
||||||
offers: ['growth', 'offers', 'discounts', 'coupons', 'promotions']
|
offers: ['growth', 'offers', 'discounts', 'coupons', 'promotions']
|
||||||
@ -25,7 +25,7 @@ const GrowthSettings: React.FC = () => {
|
|||||||
<Recommendations keywords={searchKeywords.recommendations} />
|
<Recommendations keywords={searchKeywords.recommendations} />
|
||||||
<EmbedSignupForm keywords={searchKeywords.embedSignupForm} />
|
<EmbedSignupForm keywords={searchKeywords.embedSignupForm} />
|
||||||
{hasStripeEnabled && <Offers keywords={searchKeywords.offers} />}
|
{hasStripeEnabled && <Offers keywords={searchKeywords.offers} />}
|
||||||
{hasTipsAndDonations && <TipsOrDonations keywords={searchKeywords.tips} />}
|
{hasTipsAndDonations && <TipsAndDonations keywords={searchKeywords.tips} />}
|
||||||
</SearchableSection>
|
</SearchableSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,154 @@
|
|||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import TopLevelGroup from '../../TopLevelGroup';
|
||||||
|
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||||
|
import {Button, CurrencyField, Heading, Select, SettingGroupContent, confirmIfDirty, withErrorBoundary} from '@tryghost/admin-x-design-system';
|
||||||
|
import {currencySelectGroups, getSymbol, validateCurrencyAmount} from '../../../utils/currency';
|
||||||
|
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
||||||
|
|
||||||
|
// Stripe doesn't allow amounts over 10,000 as a preset amount
|
||||||
|
const MAX_AMOUNT = 10_000;
|
||||||
|
|
||||||
|
const TipsAndDonations: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||||
|
const {
|
||||||
|
localSettings,
|
||||||
|
siteData,
|
||||||
|
updateSetting,
|
||||||
|
isEditing,
|
||||||
|
saveState,
|
||||||
|
handleSave,
|
||||||
|
handleCancel,
|
||||||
|
focusRef,
|
||||||
|
handleEditingChange,
|
||||||
|
errors,
|
||||||
|
validate,
|
||||||
|
clearError
|
||||||
|
} = useSettingGroup({
|
||||||
|
onValidate: () => {
|
||||||
|
return {
|
||||||
|
donationsSuggestedAmount: validateCurrencyAmount(suggestedAmountInCents, donationsCurrency, {maxAmount: MAX_AMOUNT})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [donationsCurrency = 'USD', donationsSuggestedAmount = '0'] = getSettingValues<string>(
|
||||||
|
localSettings,
|
||||||
|
['donations_currency', 'donations_suggested_amount']
|
||||||
|
);
|
||||||
|
|
||||||
|
const suggestedAmountInCents = parseInt(donationsSuggestedAmount);
|
||||||
|
const suggestedAmountInDollars = suggestedAmountInCents / 100;
|
||||||
|
const donateUrl = `${siteData?.url.replace(/\/$/, '')}/#/portal/support`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
validate();
|
||||||
|
}, [donationsCurrency]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const copyDonateUrl = () => {
|
||||||
|
navigator.clipboard.writeText(donateUrl);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPreview = () => {
|
||||||
|
confirmIfDirty(saveState === 'unsaved', () => window.open(donateUrl, '_blank'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const values = (
|
||||||
|
<SettingGroupContent
|
||||||
|
columns={1}
|
||||||
|
values={[
|
||||||
|
{
|
||||||
|
heading: 'Suggested amount',
|
||||||
|
key: 'suggested-amount',
|
||||||
|
value: `${getSymbol(donationsCurrency)}${suggestedAmountInDollars}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: '',
|
||||||
|
key: 'shareable-link',
|
||||||
|
value: (
|
||||||
|
<div className='w-100'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Heading level={6}>Shareable link</Heading>
|
||||||
|
</div>
|
||||||
|
<div className='w-100 group relative mt-0 flex items-center justify-between overflow-hidden border-b border-transparent pb-2 pt-1 hover:border-grey-300 dark:hover:border-grey-600'>
|
||||||
|
{donateUrl}
|
||||||
|
<div className='invisible flex gap-1 bg-white pl-1 group-hover:visible dark:bg-black'>
|
||||||
|
<Button color='clear' label={'Preview'} size='sm' onClick={openPreview} />
|
||||||
|
<Button color='light-grey' label={copied ? 'Copied' : 'Copy link'} size='sm' onClick={copyDonateUrl} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputFields = (
|
||||||
|
<SettingGroupContent columns={1}>
|
||||||
|
<div className='flex max-w-[220px] items-end gap-[.6rem]'>
|
||||||
|
<CurrencyField
|
||||||
|
error={!!errors.donationsSuggestedAmount}
|
||||||
|
hint={errors.donationsSuggestedAmount}
|
||||||
|
inputRef={focusRef}
|
||||||
|
placeholder="5"
|
||||||
|
rightPlaceholder={(
|
||||||
|
<Select
|
||||||
|
border={false}
|
||||||
|
clearBg={true}
|
||||||
|
containerClassName='w-14'
|
||||||
|
fullWidth={false}
|
||||||
|
options={currencySelectGroups()}
|
||||||
|
selectedOption={currencySelectGroups().flatMap(group => group.options).find(option => option.value === donationsCurrency)}
|
||||||
|
title='Currency'
|
||||||
|
hideTitle
|
||||||
|
isSearchable
|
||||||
|
onSelect={option => updateSetting('donations_currency', option?.value || 'USD')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
title='Suggested amount'
|
||||||
|
valueInCents={parseInt(donationsSuggestedAmount)}
|
||||||
|
onBlur={validate}
|
||||||
|
onChange={cents => updateSetting('donations_suggested_amount', cents.toString())}
|
||||||
|
onKeyDown={() => clearError('donationsSuggestedAmount')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='w-100'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Heading level={6}>Shareable link</Heading>
|
||||||
|
</div>
|
||||||
|
<div className='w-100 group relative mt-0 flex items-center justify-between overflow-hidden border-b border-transparent pb-2 pt-1 hover:border-grey-300 dark:hover:border-grey-600'>
|
||||||
|
{donateUrl}
|
||||||
|
<div className='invisible flex gap-1 bg-white pl-1 group-hover:visible dark:bg-black'>
|
||||||
|
<Button color='clear' label={'Preview'} size='sm' onClick={openPreview} />
|
||||||
|
<Button color='light-grey' label={copied ? 'Copied' : 'Copy link'} size='sm' onClick={copyDonateUrl} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingGroupContent>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopLevelGroup
|
||||||
|
description="Give your audience a one-time way to support your work, no membership required."
|
||||||
|
isEditing={isEditing}
|
||||||
|
keywords={keywords}
|
||||||
|
navid='tips-and-donations'
|
||||||
|
saveState={saveState}
|
||||||
|
testId='tips-and-donations'
|
||||||
|
title="Tips & donations"
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onEditingChange={handleEditingChange}
|
||||||
|
onSave={handleSave}
|
||||||
|
>
|
||||||
|
{isEditing ? inputFields : values}
|
||||||
|
<div className='items-center-mt-1 flex text-sm'>
|
||||||
|
All tips and donations are subject to Stripe's <a className='ml-1 text-green' href="https://ghost.org/help/tips-donations/" rel="noopener noreferrer" target="_blank"> tipping policy</a>.
|
||||||
|
</div>
|
||||||
|
</TopLevelGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withErrorBoundary(TipsAndDonations, 'Tips & donations');
|
@ -1,136 +0,0 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
|
||||||
import TopLevelGroup from '../../TopLevelGroup';
|
|
||||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
|
||||||
import {Button, CurrencyField, Heading, Select, SettingGroupContent, confirmIfDirty, withErrorBoundary} from '@tryghost/admin-x-design-system';
|
|
||||||
import {currencySelectGroups, getSymbol, validateCurrencyAmount} from '../../../utils/currency';
|
|
||||||
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
|
||||||
|
|
||||||
// Stripe doesn't allow amounts over 10,000 as a preset amount
|
|
||||||
const MAX_AMOUNT = 10_000;
|
|
||||||
|
|
||||||
const TipsOrDonations: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|
||||||
const {
|
|
||||||
localSettings,
|
|
||||||
siteData,
|
|
||||||
updateSetting,
|
|
||||||
isEditing,
|
|
||||||
saveState,
|
|
||||||
handleSave,
|
|
||||||
handleCancel,
|
|
||||||
focusRef,
|
|
||||||
handleEditingChange,
|
|
||||||
errors,
|
|
||||||
validate,
|
|
||||||
clearError
|
|
||||||
} = useSettingGroup({
|
|
||||||
onValidate: () => {
|
|
||||||
return {
|
|
||||||
donationsSuggestedAmount: validateCurrencyAmount(suggestedAmountInCents, donationsCurrency, {maxAmount: MAX_AMOUNT})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const [donationsCurrency = 'USD', donationsSuggestedAmount = '0'] = getSettingValues<string>(
|
|
||||||
localSettings,
|
|
||||||
['donations_currency', 'donations_suggested_amount']
|
|
||||||
);
|
|
||||||
|
|
||||||
const suggestedAmountInCents = parseInt(donationsSuggestedAmount);
|
|
||||||
const suggestedAmountInDollars = suggestedAmountInCents / 100;
|
|
||||||
const donateUrl = `${siteData?.url.replace(/\/$/, '')}/#/portal/support`;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
validate();
|
|
||||||
}, [donationsCurrency]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
const copyDonateUrl = () => {
|
|
||||||
navigator.clipboard.writeText(donateUrl);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openPreview = () => {
|
|
||||||
confirmIfDirty(saveState === 'unsaved', () => window.open(donateUrl, '_blank'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const values = (
|
|
||||||
<SettingGroupContent
|
|
||||||
columns={2}
|
|
||||||
values={[
|
|
||||||
{
|
|
||||||
heading: 'Suggested amount',
|
|
||||||
key: 'suggested-amount',
|
|
||||||
value: `${getSymbol(donationsCurrency)}${suggestedAmountInDollars}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '',
|
|
||||||
key: 'sharable-link',
|
|
||||||
value: (
|
|
||||||
<div className='w-100'>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<Heading level={6}>Shareable link —</Heading>
|
|
||||||
<button className='text-xs tracking-wide text-green' type="button" onClick={openPreview}>Preview</button>
|
|
||||||
</div>
|
|
||||||
<div className='w-100 group relative -m-1 mt-0 overflow-hidden rounded p-1 hover:bg-grey-50 dark:hover:bg-grey-900'>
|
|
||||||
{donateUrl}
|
|
||||||
<div className='invisible absolute right-0 top-[50%] flex translate-y-[-50%] gap-1 bg-white pl-1 group-hover:visible dark:bg-black'>
|
|
||||||
<Button color='outline' label={copied ? 'Copied' : 'Copy'} size='sm' onClick={copyDonateUrl} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const inputFields = (
|
|
||||||
<SettingGroupContent className='max-w-[180px]'>
|
|
||||||
<CurrencyField
|
|
||||||
error={!!errors.donationsSuggestedAmount}
|
|
||||||
hint={errors.donationsSuggestedAmount}
|
|
||||||
inputRef={focusRef}
|
|
||||||
placeholder="0"
|
|
||||||
rightPlaceholder={(
|
|
||||||
<Select
|
|
||||||
border={false}
|
|
||||||
containerClassName='w-14'
|
|
||||||
fullWidth={false}
|
|
||||||
options={currencySelectGroups()}
|
|
||||||
selectedOption={currencySelectGroups().flatMap(group => group.options).find(option => option.value === donationsCurrency)}
|
|
||||||
title='Currency'
|
|
||||||
hideTitle
|
|
||||||
isSearchable
|
|
||||||
onSelect={option => updateSetting('donations_currency', option?.value || 'USD')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
title='Suggested amount'
|
|
||||||
valueInCents={parseInt(donationsSuggestedAmount)}
|
|
||||||
onBlur={validate}
|
|
||||||
onChange={cents => updateSetting('donations_suggested_amount', cents.toString())}
|
|
||||||
onKeyDown={() => clearError('donationsSuggestedAmount')}
|
|
||||||
/>
|
|
||||||
</SettingGroupContent>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TopLevelGroup
|
|
||||||
description="Give your audience a one-time way to support your work, no membership required."
|
|
||||||
isEditing={isEditing}
|
|
||||||
keywords={keywords}
|
|
||||||
navid='tips-or-donations'
|
|
||||||
saveState={saveState}
|
|
||||||
testId='tips-or-donations'
|
|
||||||
title="Tips or donations"
|
|
||||||
onCancel={handleCancel}
|
|
||||||
onEditingChange={handleEditingChange}
|
|
||||||
onSave={handleSave}
|
|
||||||
>
|
|
||||||
{isEditing ? inputFields : values}
|
|
||||||
</TopLevelGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withErrorBoundary(TipsOrDonations, 'Tips or donations');
|
|
@ -46,7 +46,7 @@ test.describe('Portal', () => {
|
|||||||
test('Can donate with a fixed amount set and different currency', async ({sharedPage}) => {
|
test('Can donate with a fixed amount set and different currency', async ({sharedPage}) => {
|
||||||
await sharedPage.goto('/ghost/#/settings');
|
await sharedPage.goto('/ghost/#/settings');
|
||||||
|
|
||||||
const section = sharedPage.getByTestId('tips-or-donations');
|
const section = sharedPage.getByTestId('tips-and-donations');
|
||||||
|
|
||||||
await section.getByRole('button', {name: 'Edit'}).click();
|
await section.getByRole('button', {name: 'Edit'}).click();
|
||||||
await section.getByLabel('Suggested amount').fill('98');
|
await section.getByLabel('Suggested amount').fill('98');
|
||||||
|
Loading…
Reference in New Issue
Block a user