mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Tiers related cleanup in AdminX settings (#17504)
refs. https://github.com/TryGhost/Product/issues/3349 - added thousands separator to numbers in tiers list and preview - added dirty state handling to edit/add tier modal - applied sorting to tiers list - fixed free trial toggle bug. No default was set and didn't keep the trial value and the toggle in sync - applied a little scale down to tier preview for better proportions
This commit is contained in:
parent
005e80b466
commit
a9efd06f83
5 changed files with 57 additions and 22 deletions
|
@ -4,6 +4,7 @@ import StripeButton from '../../../admin-x-ds/settings/StripeButton';
|
|||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import TiersList from './tiers/TiersList';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {Tier} from '../../../types/api';
|
||||
import {getActiveTiers, getArchivedTiers} from '../../../utils/helpers';
|
||||
import {useTiers} from '../../providers/ServiceProvider';
|
||||
|
||||
|
@ -18,16 +19,27 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
updateRoute('stripe-connect');
|
||||
};
|
||||
|
||||
const sortTiers = (t: Tier[]) => {
|
||||
t.sort((a, b) => {
|
||||
if ((a.monthly_price as number) < (b.monthly_price as number)) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
return t;
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'active-tiers',
|
||||
title: 'Active',
|
||||
contents: (<TiersList tab='active-tiers' tiers={activeTiers} updateTier={updateTier} />)
|
||||
contents: (<TiersList tab='active-tiers' tiers={sortTiers(activeTiers)} updateTier={updateTier} />)
|
||||
},
|
||||
{
|
||||
id: 'archived-tiers',
|
||||
title: 'Archived',
|
||||
contents: (<TiersList tab='archive-tiers' tiers={archivedTiers} updateTier={updateTier} />)
|
||||
contents: (<TiersList tab='archive-tiers' tiers={sortTiers(archivedTiers)} updateTier={updateTier} />)
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
return error;
|
||||
};
|
||||
|
||||
const {formState, updateForm, handleSave} = useForm<TierFormState>({
|
||||
const {formState, saveState, updateForm, handleSave} = useForm<TierFormState>({
|
||||
initialState: {
|
||||
...(tier || {}),
|
||||
monthly_price: tier?.monthly_price ? currencyToDecimal(tier.monthly_price).toString() : '',
|
||||
|
@ -97,10 +97,21 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
return value.match(/[\d]+\.?[\d]{0,2}/)?.[0] || '';
|
||||
};
|
||||
|
||||
const toggleFreeTrial = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setHasFreeTrial(true);
|
||||
updateForm(state => ({...state, trial_days: tier?.trial_days ? tier?.trial_days.toString() : '7'}));
|
||||
} else {
|
||||
setHasFreeTrial(false);
|
||||
updateForm(state => ({...state, trial_days: '0'}));
|
||||
}
|
||||
};
|
||||
|
||||
return <Modal
|
||||
afterClose={() => {
|
||||
updateRoute('tiers');
|
||||
}}
|
||||
dirty={saveState === 'unsaved'}
|
||||
okLabel='Save & close'
|
||||
size='lg'
|
||||
testId='tier-detail-modal'
|
||||
|
@ -116,6 +127,10 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
}
|
||||
|
||||
handleSave();
|
||||
|
||||
if (saveState !== 'unsaved') {
|
||||
modal.remove();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='-mb-8 mt-8 flex items-start gap-8'>
|
||||
|
@ -186,7 +201,7 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
</div>
|
||||
<div className='basis-1/2'>
|
||||
<div className='mb-1 flex h-6 flex-col justify-center'>
|
||||
<Toggle label='Add a free trial' labelStyle='heading' onChange={e => setHasFreeTrial(e.target.checked)} />
|
||||
<Toggle checked={hasFreeTrial} label='Add a free trial' labelStyle='heading' onChange={toggleFreeTrial} />
|
||||
</div>
|
||||
<TextField
|
||||
disabled={!hasFreeTrial}
|
||||
|
|
|
@ -6,6 +6,7 @@ import useSettingGroup from '../../../../hooks/useSettingGroup';
|
|||
import {Tier} from '../../../../types/api';
|
||||
import {getSettingValues} from '../../../../utils/helpers';
|
||||
import {getSymbol} from '../../../../utils/currency';
|
||||
import {numberWithCommas} from '../../../../utils/helpers';
|
||||
|
||||
export type TierFormState = Partial<Omit<Tier, 'monthly_price' | 'yearly_price' | 'trial_days'>> & {
|
||||
monthly_price: string;
|
||||
|
@ -95,23 +96,25 @@ const TierDetailPreview: React.FC<TierDetailPreviewProps> = ({tier, isFreeTier})
|
|||
<Button className={`ml-2 ${showingYearly === true ? 'text-grey-900' : 'text-grey-500'}`} label="Yearly" link onClick={() => setShowingYearly(true)} />
|
||||
</div>}
|
||||
</div>
|
||||
<div className="flex-column relative flex min-h-[200px] w-full max-w-[420px] items-start justify-stretch rounded border border-grey-200 bg-white p-8">
|
||||
<div className="min-h-[56px] w-full">
|
||||
<h4 className={`-mt-1 mb-0 w-full break-words text-lg font-semibold leading-tight text-pink ${!name && 'opacity-30'}`}>{name || 'Bronze'}</h4>
|
||||
<div className="mt-4 flex w-full flex-row flex-wrap items-end justify-between gap-x-1 gap-y-[10px]">
|
||||
<div className={`flex flex-wrap text-black ${!yearlyPrice && !monthlyPrice && !isFreeTier && 'opacity-30'}`}>
|
||||
<span className="self-start text-[2.7rem] font-bold uppercase leading-[1.115]">{currencySymbol}</span>
|
||||
<span className="break-all text-[3.4rem] font-bold leading-none tracking-tight">{showingYearly ? yearlyPrice : monthlyPrice}</span>
|
||||
{!isFreeTier && <span className="ml-1 self-end text-[1.5rem] leading-snug text-grey-800">/{showingYearly ? 'year' : 'month'}</span>}
|
||||
<div className='border border-grey-200'>
|
||||
<div className="flex-column relative flex min-h-[200px] w-full max-w-[420px] scale-90 items-start justify-stretch rounded bg-white p-4">
|
||||
<div className="min-h-[56px] w-full">
|
||||
<h4 className={`-mt-1 mb-0 w-full break-words text-lg font-semibold leading-tight text-pink ${!name && 'opacity-30'}`}>{name || 'Bronze'}</h4>
|
||||
<div className="mt-4 flex w-full flex-row flex-wrap items-end justify-between gap-x-1 gap-y-[10px]">
|
||||
<div className={`flex flex-wrap text-black ${!yearlyPrice && !monthlyPrice && !isFreeTier && 'opacity-30'}`}>
|
||||
<span className="self-start text-[2.7rem] font-bold uppercase leading-[1.115]">{currencySymbol}</span>
|
||||
<span className="break-all text-[3.4rem] font-bold leading-none tracking-tight">{showingYearly ? numberWithCommas(yearlyPrice) : numberWithCommas(monthlyPrice)}</span>
|
||||
{!isFreeTier && <span className="ml-1 self-end text-[1.5rem] leading-snug text-grey-800">/{showingYearly ? 'year' : 'month'}</span>}
|
||||
</div>
|
||||
<TrialDaysLabel trialDays={trialDays} />
|
||||
</div>
|
||||
<TrialDaysLabel trialDays={trialDays} />
|
||||
{(showingYearly && yearlyDiscount > 0) && <DiscountLabel discount={yearlyDiscount} />}
|
||||
</div>
|
||||
{(showingYearly && yearlyDiscount > 0) && <DiscountLabel discount={yearlyDiscount} />}
|
||||
</div>
|
||||
<div className="flex-column flex w-full flex-1">
|
||||
<div className="flex-1">
|
||||
<div className={`mt-4 w-full text-[1.55rem] font-semibold leading-snug text-grey-900 ${!description && 'opacity-30'}`}>{description || (isFreeTier ? `Free preview of ${siteTitle}` : 'Full access to premium content')}</div>
|
||||
<TierBenefits benefits={benefits} />
|
||||
<div className="flex-column flex w-full flex-1">
|
||||
<div className="flex-1">
|
||||
<div className={`mt-4 w-full text-[1.55rem] font-semibold leading-snug text-grey-900 ${!description && 'opacity-30'}`}>{description || (isFreeTier ? `Free preview of ${siteTitle}` : 'Full access to premium content')}</div>
|
||||
<TierBenefits benefits={benefits} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@ import TierDetailModal from './TierDetailModal';
|
|||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {Tier} from '../../../../types/api';
|
||||
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
|
||||
import {numberWithCommas} from '../../../../utils/helpers';
|
||||
|
||||
interface TiersListProps {
|
||||
tab?: string;
|
||||
|
@ -35,11 +36,11 @@ const TierCard: React.FC<TierCardProps> = ({
|
|||
}}>
|
||||
<div className='text-[1.65rem] font-bold leading-tight tracking-tight text-pink'>{tier.name}</div>
|
||||
<div className='mt-2 flex items-baseline'>
|
||||
<span className="ml-1 translate-y-[-3px] text-xl font-bold uppercase">{currencySymbol}</span>
|
||||
<span className='text-2xl font-bold tracking-tighter'>{currencyToDecimal(tier.monthly_price || 0)}</span>
|
||||
<span className="ml-1 translate-y-[-3px] text-md font-bold uppercase">{currencySymbol}</span>
|
||||
<span className='text-xl font-bold tracking-tighter'>{numberWithCommas(currencyToDecimal(tier.monthly_price || 0))}</span>
|
||||
{(tier.monthly_price && tier.monthly_price > 0) && <span className='text-sm text-grey-700'>/month</span>}
|
||||
</div>
|
||||
<div className='mt-2 line-clamp-2 text-sm font-medium'>
|
||||
<div className='mt-2 line-clamp-2 text-[1.4rem] font-medium'>
|
||||
{tier.description || <span className='opacity-50'>No description</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -143,3 +143,7 @@ export function getArchivedTiers(tiers: Tier[]) {
|
|||
return !tier.active;
|
||||
});
|
||||
}
|
||||
|
||||
export function numberWithCommas(x: number) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
Loading…
Add table
Reference in a new issue