mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
AdminX UI fixes (#18110)
refs. https://github.com/TryGhost/Product/issues/3349 - fix tiers save & close bug - show trial days on tier card - removed 0's from monthly and yearly tier prices - added color to learn more link in tier modal - set background to white in newsletter preview - fixed newsletter default sender data - removed underline in "View in browser" link in newsletter preview - updated copy in newsletters - added Integrations' active indicator - scrolling menu under searchbar to give search more prominance - updated Portal modal buttons to be consistent with design settings - fixed bug in AdminX loading Orb so that it actually starts auto-playing
This commit is contained in:
parent
d7835ad5ed
commit
dff8c38547
25 changed files with 79 additions and 48 deletions
|
@ -29,7 +29,7 @@ const queryClient = new QueryClient({
|
|||
|
||||
function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, darkMode = false}: AppProps) {
|
||||
const appClassName = clsx(
|
||||
'admin-x-settings h-[100vh] w-full overflow-y-auto',
|
||||
'admin-x-settings h-[100vh] w-full overflow-y-auto overflow-x-hidden',
|
||||
darkMode && 'dark'
|
||||
);
|
||||
|
||||
|
|
|
@ -52,12 +52,11 @@ const MainContent: React.FC = () => {
|
|||
<div className='-mx-6 h-[84px] bg-white px-6 tablet:m-0 tablet:bg-transparent tablet:p-0'>
|
||||
<Heading>Settings</Heading>
|
||||
</div>
|
||||
<div className="relative mt-[-32px] w-full overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:hidden after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-[''] dark:after:from-black tablet:w-[260px] tablet:after:!visible tablet:after:!block">
|
||||
<div className="relative mt-[-32px] w-full overflow-x-hidden">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex-auto pt-[3vmin] tablet:ml-[300px] tablet:pt-[85px]">
|
||||
{/* <div className='pointer-events-none fixed inset-x-0 top-0 z-[5] hidden h-[80px] bg-gradient-to-t from-transparent to-white to-60% dark:to-black tablet:!visible tablet:!block'></div> */}
|
||||
<div className="relative flex-auto pt-[10vmin] tablet:ml-[300px] tablet:pt-[85px]">
|
||||
<Settings />
|
||||
</div>
|
||||
</Page>
|
||||
|
|
|
@ -3,7 +3,7 @@ import TextField, {TextFieldProps} from './TextField';
|
|||
import {currencyFromDecimal, currencyToDecimal} from '../../../utils/currency';
|
||||
|
||||
export type CurrencyFieldProps = Omit<TextFieldProps, 'type' | 'onChange' | 'value'> & {
|
||||
valueInCents?: number;
|
||||
valueInCents?: number | '';
|
||||
currency?: string;
|
||||
onChange?: (cents: number) => void;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ const CurrencyField: React.FC<CurrencyFieldProps> = ({
|
|||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const [localValue, setLocalValue] = useState(currencyToDecimal(valueInCents || 0).toString());
|
||||
const [localValue, setLocalValue] = useState(valueInCents === '' ? '' : currencyToDecimal(valueInCents || 0).toString());
|
||||
|
||||
// While the user is editing we allow more lenient input, e.g. "1.32.566" to make it easier to type and change
|
||||
const stripNonNumeric = (input: string) => input.replace(/[^\d.]+/g, '');
|
||||
|
|
|
@ -38,12 +38,12 @@ const Sidebar: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='no-scrollbar tablet:h-[calc(100vh-5vmin-84px)] tablet:w-[240px] tablet:overflow-y-scroll'>
|
||||
<div className='relative mb-10 md:pt-4 tablet:pt-[32px]'>
|
||||
<div>
|
||||
<div className='relative md:pt-4 tablet:h-[64px] tablet:pt-[32px]'>
|
||||
<Icon className='absolute top-2 md:top-6 tablet:top-10' colorClass='text-grey-500' name='magnifying-glass' size='sm' />
|
||||
<TextField autoComplete="off" className='border-b border-grey-500 bg-transparent px-3 py-1.5 pl-[24px] text-sm dark:text-white' placeholder="Search" title="Search" value={filter} hideTitle unstyled onChange={updateSearch} />
|
||||
</div>
|
||||
<div className="hidden tablet:!visible tablet:!block">
|
||||
<div className="no-scrollbar hidden pt-10 tablet:!visible tablet:!block tablet:h-[calc(100vh-5vmin-84px-64px)] tablet:w-[240px] tablet:overflow-y-auto">
|
||||
<SettingNavSection keywords={Object.values(generalSearchKeywords).flat()} title="General">
|
||||
<SettingNavItem keywords={generalSearchKeywords.titleAndDescription} navid='title-and-description' title="Title and description" onClick={handleSectionClick} />
|
||||
<SettingNavItem keywords={generalSearchKeywords.timeZone} navid='timezone' title="Timezone" onClick={handleSectionClick} />
|
||||
|
|
|
@ -16,6 +16,7 @@ import {ReactComponent as PinturaIcon} from '../../../assets/icons/pintura.svg';
|
|||
import {ReactComponent as SlackIcon} from '../../../assets/icons/slack.svg';
|
||||
import {ReactComponent as UnsplashIcon} from '../../../assets/icons/unsplash.svg';
|
||||
import {ReactComponent as ZapierIcon} from '../../../assets/icons/zapier.svg';
|
||||
import {getSettingValues} from '../../../api/settings';
|
||||
import {showToast} from '../../../admin-x-ds/global/Toast';
|
||||
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||
|
||||
|
@ -25,6 +26,7 @@ interface IntegrationItemProps {
|
|||
detail: string,
|
||||
action: () => void;
|
||||
onDelete?: () => void;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
custom?: boolean;
|
||||
|
@ -36,6 +38,7 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
|
|||
detail,
|
||||
action,
|
||||
onDelete,
|
||||
active,
|
||||
disabled,
|
||||
testId,
|
||||
custom = false
|
||||
|
@ -65,7 +68,7 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
|
|||
detail={detail}
|
||||
hideActions={!disabled}
|
||||
testId={testId}
|
||||
title={title}
|
||||
title={active ? <span className='inline-flex items-center gap-1'>{title} <span className='inline-flex items-center rounded-full bg-[rgba(48,207,67,0.15)] px-1.5 py-0.5 text-2xs font-semibold uppercase tracking-wide text-green'>Active</span></span> : title}
|
||||
onClick={handleClick}
|
||||
/>;
|
||||
};
|
||||
|
@ -80,6 +83,9 @@ const BuiltInIntegrations: React.FC = () => {
|
|||
|
||||
const zapierDisabled = config.hostSettings?.limits?.customIntegrations?.disabled;
|
||||
|
||||
const {settings} = useGlobalData();
|
||||
const [ampEnabled, unsplashEnabled, pinturaEnabled, firstPromoterEnabled, slackUrl, slackUsername] = getSettingValues<boolean>(settings, ['amp', 'unsplash', 'pintura', 'firstpromoter', 'slack_url', 'slack_username']);
|
||||
|
||||
return (
|
||||
<List titleSeparator={false}>
|
||||
<IntegrationItem
|
||||
|
@ -96,6 +102,7 @@ const BuiltInIntegrations: React.FC = () => {
|
|||
action={() => {
|
||||
openModal('integrations/slack');
|
||||
}}
|
||||
active={slackUrl && slackUsername}
|
||||
detail='A messaging app for teams'
|
||||
icon={<SlackIcon className='h-8 w-8' />}
|
||||
title='Slack' />
|
||||
|
@ -104,6 +111,7 @@ const BuiltInIntegrations: React.FC = () => {
|
|||
action={() => {
|
||||
openModal('integrations/amp');
|
||||
}}
|
||||
active={ampEnabled}
|
||||
detail='Google Accelerated Mobile Pages'
|
||||
icon={<AmpIcon className='h-8 w-8' />}
|
||||
title='AMP' />
|
||||
|
@ -112,6 +120,7 @@ const BuiltInIntegrations: React.FC = () => {
|
|||
action={() => {
|
||||
openModal('integrations/unsplash');
|
||||
}}
|
||||
active={unsplashEnabled}
|
||||
detail='Beautiful, free photos'
|
||||
icon={<UnsplashIcon className='h-8 w-8' />}
|
||||
title='Unsplash' />
|
||||
|
@ -120,6 +129,7 @@ const BuiltInIntegrations: React.FC = () => {
|
|||
action={() => {
|
||||
openModal('integrations/firstpromoter');
|
||||
}}
|
||||
active={firstPromoterEnabled}
|
||||
detail='Launch your member referral program'
|
||||
icon={<FirstPromoterIcon className='h-8 w-8' />}
|
||||
title='FirstPromoter' />
|
||||
|
@ -128,6 +138,7 @@ const BuiltInIntegrations: React.FC = () => {
|
|||
action={() => {
|
||||
openModal('integrations/pintura');
|
||||
}}
|
||||
active={pinturaEnabled}
|
||||
detail='Advanced image editing' icon=
|
||||
{<PinturaIcon className='h-8 w-8' />} title
|
||||
='Pintura' />
|
||||
|
|
|
@ -90,7 +90,7 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save integration, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save integration, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -43,7 +43,7 @@ const SlackModal = NiceModal.create(() => {
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save Slack settings, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save Slack settings, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -66,7 +66,7 @@ const SlackModal = NiceModal.create(() => {
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save Slack settings, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save Slack settings, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -67,7 +67,7 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save webhook, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save webhook, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -83,7 +83,7 @@ const AddNewsletterModal: React.FC<AddNewsletterModalProps> = () => {
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save newsletter, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -20,6 +20,7 @@ import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
|
|||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
import validator from 'validator';
|
||||
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '../../../../api/newsletters';
|
||||
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
|
||||
|
@ -44,6 +45,8 @@ const Sidebar: React.FC<{
|
|||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
const [selectedTab, setSelectedTab] = useState('generalSettings');
|
||||
const hasEmailCustomization = useFeatureFlag('emailCustomization');
|
||||
const {localSettings} = useSettingGroup();
|
||||
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
|
||||
|
||||
const replyToEmails = [
|
||||
{label: `Newsletter address (${fullEmailAddress(newsletter.sender_email || 'noreply', siteData)})`, value: 'newsletter'},
|
||||
|
@ -85,11 +88,11 @@ const Sidebar: React.FC<{
|
|||
<TextArea rows={2} title="Description" value={newsletter.description || ''} onChange={e => updateNewsletter({description: e.target.value})} />
|
||||
</Form>
|
||||
<Form className='mt-6' gap='sm' margins='lg' title='Email addresses'>
|
||||
<TextField placeholder="Ghost" title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
|
||||
<TextField placeholder={siteTitle} title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
|
||||
<TextField
|
||||
error={Boolean(errors.sender_email)}
|
||||
hint={errors.sender_email}
|
||||
placeholder="noreply@localhost"
|
||||
placeholder={fullEmailAddress(newsletter.sender_email || 'noreply', siteData)}
|
||||
title="Sender email address"
|
||||
value={newsletter.sender_email || ''}
|
||||
onBlur={validate}
|
||||
|
@ -143,21 +146,21 @@ const Sidebar: React.FC<{
|
|||
checked={newsletter.show_header_icon}
|
||||
direction="rtl"
|
||||
label='Publication icon'
|
||||
labelStyle='value'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_header_icon: e.target.checked})}
|
||||
/>}
|
||||
<Toggle
|
||||
checked={newsletter.show_header_title}
|
||||
direction="rtl"
|
||||
label='Publication title'
|
||||
labelStyle='value'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_header_title: e.target.checked})}
|
||||
/>
|
||||
<Toggle
|
||||
checked={newsletter.show_header_name}
|
||||
direction="rtl"
|
||||
label='Newsletter name'
|
||||
labelStyle='value'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateNewsletter({show_header_name: e.target.checked})}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
|
@ -432,7 +435,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter}> = ({newsl
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save newsletter, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -93,6 +93,7 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) =>
|
|||
|
||||
return <NewsletterPreviewContent
|
||||
authorPlaceholder={currentUser.name || currentUser.email}
|
||||
backgroundColor={colors.backgroundColor || '#ffffff'}
|
||||
bodyFontCategory={newsletter.body_font_category}
|
||||
footerContent={newsletter.footer_content}
|
||||
headerIcon={newsletter.show_header_icon ? icon : undefined}
|
||||
|
|
|
@ -124,7 +124,7 @@ const NewsletterPreviewContent: React.FC<{
|
|||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="pb-2 underline" style={{color: secondaryTextColor}}><span>View in browser</span></p>
|
||||
<p className="pb-2" style={{color: secondaryTextColor}}><span>View in browser</span></p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -782,7 +782,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
|||
if (error) {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save user, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save user, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
setSaveState('');
|
||||
return;
|
||||
|
|
|
@ -25,9 +25,9 @@ const AccountPage: React.FC<{
|
|||
setValue(fullEmailAddress(settingValue, siteData!));
|
||||
};
|
||||
|
||||
return <Form marginTop>
|
||||
return <div className='mt-7'><Form>
|
||||
<TextField title='Support email address' value={value} onBlur={updateSupportAddress} onChange={e => setValue(e.target.value)} />
|
||||
</Form>;
|
||||
</Form></div>;
|
||||
};
|
||||
|
||||
export default AccountPage;
|
||||
|
|
|
@ -62,7 +62,7 @@ const LookAndFeel: React.FC<{
|
|||
setUploadedIcon(undefined);
|
||||
};
|
||||
|
||||
return <Form marginTop>
|
||||
return <div className='mt-7'><Form>
|
||||
<Toggle
|
||||
checked={Boolean(portalButton)}
|
||||
label='Show portal button'
|
||||
|
@ -116,7 +116,7 @@ const LookAndFeel: React.FC<{
|
|||
onChange={e => updateSetting('portal_button_signup_text', e.target.value)}
|
||||
/>
|
||||
}
|
||||
</Form>;
|
||||
</Form></div>;
|
||||
};
|
||||
|
||||
export default LookAndFeel;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import AccountPage from './AccountPage';
|
||||
import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal';
|
||||
import LookAndFeel from './LookAndFeel';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import PortalPreview from './PortalPreview';
|
||||
import React, {useState} from 'react';
|
||||
import SignupOptions from './SignupOptions';
|
||||
|
@ -61,7 +61,6 @@ const Sidebar: React.FC<{
|
|||
};
|
||||
|
||||
const PortalModal: React.FC = () => {
|
||||
const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const [selectedPreviewTab, setSelectedPreviewTab] = useState('signup');
|
||||
|
@ -152,7 +151,7 @@ const PortalModal: React.FC = () => {
|
|||
{id: 'account', title: 'Account page'},
|
||||
{id: 'links', title: 'Links'}
|
||||
];
|
||||
let okLabel = 'Save & close';
|
||||
let okLabel = 'Save';
|
||||
if (saveState === 'saving') {
|
||||
okLabel = 'Saving...';
|
||||
} else if (saveState === 'saved') {
|
||||
|
@ -163,6 +162,7 @@ const PortalModal: React.FC = () => {
|
|||
afterClose={() => {
|
||||
updateRoute('portal');
|
||||
}}
|
||||
cancelLabel='Close'
|
||||
deviceSelector={false}
|
||||
dirty={saveState === 'unsaved'}
|
||||
okLabel={okLabel}
|
||||
|
@ -176,8 +176,6 @@ const PortalModal: React.FC = () => {
|
|||
onOk={async () => {
|
||||
if (!Object.values(errors).filter(Boolean).length) {
|
||||
await handleSave();
|
||||
updateRoute('portal');
|
||||
modal.remove();
|
||||
}
|
||||
}}
|
||||
onSelectURL={onSelectURL}
|
||||
|
|
|
@ -83,7 +83,7 @@ const SignupOptions: React.FC<{
|
|||
});
|
||||
}
|
||||
|
||||
return <Form marginTop>
|
||||
return <div className='mt-7'><Form>
|
||||
<Toggle
|
||||
checked={Boolean(portalName)}
|
||||
disabled={isDisabled}
|
||||
|
@ -141,7 +141,7 @@ const SignupOptions: React.FC<{
|
|||
labelStyle='heading'
|
||||
onChange={e => updateSetting('portal_signup_checkbox_required', e.target.checked)}
|
||||
/>}
|
||||
</Form>;
|
||||
</Form></div>;
|
||||
};
|
||||
|
||||
export default SignupOptions;
|
||||
|
|
|
@ -121,11 +121,16 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
|||
if (Object.values(validators).filter(validator => validator()).length) {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save tier, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save tier, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveState !== 'unsaved') {
|
||||
updateRoute('tiers');
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
if (await handleSave()) {
|
||||
updateRoute('tiers');
|
||||
}
|
||||
|
@ -173,7 +178,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
|||
placeholder='1'
|
||||
rightPlaceholder={`${formState.currency}/month`}
|
||||
title='Monthly price'
|
||||
valueInCents={formState.monthly_price || 0}
|
||||
valueInCents={formState.monthly_price || ''}
|
||||
hideTitle
|
||||
onBlur={() => validators.monthly_price()}
|
||||
onChange={price => updateForm(state => ({...state, monthly_price: price}))}
|
||||
|
@ -184,7 +189,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
|||
placeholder='10'
|
||||
rightPlaceholder={`${formState.currency}/year`}
|
||||
title='Yearly price'
|
||||
valueInCents={formState.yearly_price || 0}
|
||||
valueInCents={formState.yearly_price || ''}
|
||||
hideTitle
|
||||
onBlur={() => validators.yearly_price()}
|
||||
onChange={price => updateForm(state => ({...state, yearly_price: price}))}
|
||||
|
@ -197,9 +202,9 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
|||
</div>
|
||||
<TextField
|
||||
disabled={!hasFreeTrial}
|
||||
hint={<>
|
||||
Members will be subscribed at full price once the trial ends. <a href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
|
||||
</>}
|
||||
hint={<div className='mt-1'>
|
||||
Members will be subscribed at full price once the trial ends. <a className='text-green' href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
|
||||
</div>}
|
||||
placeholder='0'
|
||||
rightPlaceholder='days'
|
||||
title='Trial days'
|
||||
|
|
|
@ -2,6 +2,7 @@ import Button from '../../../../admin-x-ds/global/Button';
|
|||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import Icon from '../../../../admin-x-ds/global/Icon';
|
||||
import React, {useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
import {TierFormState} from './TierDetailModal';
|
||||
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
|
||||
|
@ -13,12 +14,18 @@ interface TierDetailPreviewProps {
|
|||
isFreeTier: boolean;
|
||||
}
|
||||
|
||||
const TrialDaysLabel: React.FC<{trialDays: number}> = ({trialDays}) => {
|
||||
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="relative -mr-1 -mt-1 whitespace-nowrap rounded-full px-2.5 py-1.5 text-sm font-semibold leading-none tracking-wide text-grey-900">
|
||||
<span className={containerClassName}>
|
||||
<span className="absolute inset-0 block rounded-full bg-pink opacity-20"></span>
|
||||
{trialDays} days free
|
||||
</span>
|
||||
|
|
|
@ -4,6 +4,7 @@ import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
|
|||
import React from 'react';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {Tier, useEditTier} from '../../../../api/tiers';
|
||||
import {TrialDaysLabel} from './TierDetailPreview';
|
||||
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
|
||||
import {numberWithCommas} from '../../../../utils/helpers';
|
||||
|
||||
|
@ -35,6 +36,12 @@ const TierCard: React.FC<TierCardProps> = ({tier}) => {
|
|||
<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>
|
||||
{tier.trial_days ?
|
||||
<div className='mb-4 mt-1'>
|
||||
<TrialDaysLabel size='sm' trialDays={tier.trial_days}/>
|
||||
</div>
|
||||
: ''
|
||||
}
|
||||
<div className='mt-2 line-clamp-2 text-[1.4rem] font-medium'>
|
||||
{tier.description || <span className='opacity-50'>No description</span>}
|
||||
</div>
|
||||
|
|
|
@ -136,7 +136,7 @@ const AddRecommendationModal: React.FC<AddRecommendationModalProps> = ({recommen
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'One or more fields have errors, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'One or more fields have errors, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -104,7 +104,7 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'One or more fields have errors, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'One or more fields have errors, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -70,7 +70,7 @@ const EditRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'One or more fields have errors, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'One or more fields have errors, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -105,7 +105,7 @@ const useSettingGroup = ({onValidate}: {onValidate?: () => ErrorMessages} = {}):
|
|||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save settings! One or more fields have errors, please double check that you\'ve filled in all mandatory fields.'
|
||||
message: 'Can\'t save settings! One or more fields have errors, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -297,12 +297,12 @@ export default class AdminXSettings extends Component {
|
|||
justifyContent: 'center',
|
||||
paddingBottom: '8vh'
|
||||
}}>
|
||||
<video width="100" height="100" loop autoplay muted playsinline preload="metadata" style={{
|
||||
<video width="100" height="100" loop autoPlay muted playsInline preload="metadata" style={{
|
||||
width: '100px',
|
||||
height: '100px'
|
||||
}}>
|
||||
<source src="assets/videos/logo-loader.mp4" type="video/mp4" />
|
||||
<div class="gh-loading-spinner"></div>
|
||||
<div className="gh-loading-spinner"></div>
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue