0
Fork 0
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:
Peter Zimon 2023-07-26 16:13:07 +02:00 committed by GitHub
parent 005e80b466
commit a9efd06f83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 22 deletions

View file

@ -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} />)
}
];

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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, ',');
}