TryGhost/Ghost

View on GitHub
apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailPreview.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
import React, {useState} from 'react';
import clsx from 'clsx';
import {Button, Heading, Icon} from '@tryghost/admin-x-design-system';
import {TierFormState} from './TierDetailModal';
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
import {numberWithCommas} from '../../../../utils/helpers';

interface TierDetailPreviewProps {
    tier: TierFormState;
    isFreeTier: boolean;
}

export const TrialDaysLabel: React.FC<{size?: 'sm' | 'md'; trialDays: number;}> = ({size = 'md', trialDays}) => {
    if (!trialDays) {
        return null;
    }

    const containerClassName = clsx(
        size === 'sm' ? 'px-1.5 py-0.5 text-xs' : 'px-2.5 py-1.5 text-sm',
        'relative -mr-1 -mt-1 whitespace-nowrap rounded-full font-semibold leading-none tracking-wide text-grey-900'
    );

    return (
        <span className={containerClassName}>
            <span className="absolute inset-0 block rounded-full bg-pink opacity-20"></span>
            <span className='dark:text-pink'>{trialDays} days free</span>
        </span>
    );
};

const TierBenefits: React.FC<{benefits: string[]}> = ({benefits}) => {
    if (!benefits?.length) {
        return (
            <div className="mt-4 w-full text-md leading-snug text-grey-900 opacity-30">
                <div className="mb-2.5 flex items-start">
                    <Icon className="mr-[10px] mt-[3px] !h-3.5 !w-3.5 min-w-[14px] overflow-visible !stroke-[3px]" name='check' />
                    <div>Expert analysis</div>
                </div>
            </div>
        );
    }
    return (
        <>
            {
                benefits.map((benefit) => {
                    return (
                        <div key={benefit} className="mt-4 w-full text-md leading-snug text-grey-900">
                            <div className="mb-2.5 flex items-start">
                                <Icon className="mr-[10px] mt-[3px] !h-3.5 !w-3.5 min-w-[14px] overflow-visible !stroke-[3px]" name='check' />
                                <div>{benefit}</div>
                            </div>
                        </div>
                    );
                })
            }
        </>
    );
};

const DiscountLabel: React.FC<{discount: number}> = ({discount}) => {
    if (!discount) {
        return null;
    }
    return (
        <span className="mt-1 text-sm font-semibold leading-none text-pink">{discount}% discount</span>
    );
};

const TierDetailPreview: React.FC<TierDetailPreviewProps> = ({tier, isFreeTier}) => {
    const [showingYearly, setShowingYearly] = useState(false);

    const name = tier?.name || '';
    const description = tier?.description || '';
    const trialDays = parseFloat(tier?.trial_days || '0');
    const currency = tier?.currency || 'USD';
    const currencySymbol = currency ? getSymbol(currency) : '$';
    const benefits = tier?.benefits || [];

    const defaultMonthlyPrice = isFreeTier ? 0 : 500;
    const defaultYearlyPrice = isFreeTier ? 0 : 5000;
    const monthlyPrice = currencyToDecimal(tier?.monthly_price ?? defaultMonthlyPrice);
    const yearlyPrice = currencyToDecimal(tier?.yearly_price ?? defaultYearlyPrice);
    const yearlyDiscount = tier?.monthly_price && tier?.yearly_price
        ? Math.ceil(((monthlyPrice * 12 - yearlyPrice) / (monthlyPrice * 12)) * 100)
        : 0;

    return (
        <div data-testid="tier-preview">
            <div className="flex items-baseline justify-between">
                <Heading className="pb-2" level={6} grey>{isFreeTier ? 'Free membership preview' : 'Tier preview'}</Heading>
                {!isFreeTier && <div className="flex gap-1">
                    <Button className={`${showingYearly === true ? 'text-grey-500' : 'text-grey-900 dark:text-white'}`} label="Monthly" link unstyled onClick={() => setShowingYearly(false)} />
                    <Button className={`ml-2 ${showingYearly === true ? 'text-grey-900 dark:text-white' : 'text-grey-500'}`} label="Yearly" link unstyled onClick={() => setShowingYearly(true)} />
                </div>}
            </div>
            <div className='rounded-sm border border-grey-200 bg-white dark:border-transparent'>
                <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 || (isFreeTier ? 'Free' : '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 ${((showingYearly && tier?.yearly_price === undefined) || (!showingYearly && tier?.monthly_price === undefined)) && !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>
                        {(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` : 'Full access to premium content')}</div>
                            <TierBenefits benefits={benefits} />
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
};

export default TierDetailPreview;