0
Fork 0
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:
Peter Zimon 2023-09-14 13:04:31 +03:00 committed by GitHub
parent d7835ad5ed
commit dff8c38547
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 79 additions and 48 deletions

View file

@ -29,7 +29,7 @@ const queryClient = new QueryClient({
function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, darkMode = false}: AppProps) { function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, darkMode = false}: AppProps) {
const appClassName = clsx( 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' darkMode && 'dark'
); );

View file

@ -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'> <div className='-mx-6 h-[84px] bg-white px-6 tablet:m-0 tablet:bg-transparent tablet:p-0'>
<Heading>Settings</Heading> <Heading>Settings</Heading>
</div> </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 /> <Sidebar />
</div> </div>
</div> </div>
<div className="relative flex-auto pt-[3vmin] tablet:ml-[300px] tablet:pt-[85px]"> <div className="relative flex-auto pt-[10vmin] 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> */}
<Settings /> <Settings />
</div> </div>
</Page> </Page>

View file

@ -3,7 +3,7 @@ import TextField, {TextFieldProps} from './TextField';
import {currencyFromDecimal, currencyToDecimal} from '../../../utils/currency'; import {currencyFromDecimal, currencyToDecimal} from '../../../utils/currency';
export type CurrencyFieldProps = Omit<TextFieldProps, 'type' | 'onChange' | 'value'> & { export type CurrencyFieldProps = Omit<TextFieldProps, 'type' | 'onChange' | 'value'> & {
valueInCents?: number; valueInCents?: number | '';
currency?: string; currency?: string;
onChange?: (cents: number) => void; onChange?: (cents: number) => void;
} }
@ -20,7 +20,7 @@ const CurrencyField: React.FC<CurrencyFieldProps> = ({
onChange, onChange,
...props ...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 // 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, ''); const stripNonNumeric = (input: string) => input.replace(/[^\d.]+/g, '');

View file

@ -38,12 +38,12 @@ const Sidebar: React.FC = () => {
}; };
return ( return (
<div className='no-scrollbar tablet:h-[calc(100vh-5vmin-84px)] tablet:w-[240px] tablet:overflow-y-scroll'> <div>
<div className='relative mb-10 md:pt-4 tablet:pt-[32px]'> <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' /> <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} /> <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>
<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"> <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.titleAndDescription} navid='title-and-description' title="Title and description" onClick={handleSectionClick} />
<SettingNavItem keywords={generalSearchKeywords.timeZone} navid='timezone' title="Timezone" onClick={handleSectionClick} /> <SettingNavItem keywords={generalSearchKeywords.timeZone} navid='timezone' title="Timezone" onClick={handleSectionClick} />

View file

@ -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 SlackIcon} from '../../../assets/icons/slack.svg';
import {ReactComponent as UnsplashIcon} from '../../../assets/icons/unsplash.svg'; import {ReactComponent as UnsplashIcon} from '../../../assets/icons/unsplash.svg';
import {ReactComponent as ZapierIcon} from '../../../assets/icons/zapier.svg'; import {ReactComponent as ZapierIcon} from '../../../assets/icons/zapier.svg';
import {getSettingValues} from '../../../api/settings';
import {showToast} from '../../../admin-x-ds/global/Toast'; import {showToast} from '../../../admin-x-ds/global/Toast';
import {useGlobalData} from '../../providers/GlobalDataProvider'; import {useGlobalData} from '../../providers/GlobalDataProvider';
@ -25,6 +26,7 @@ interface IntegrationItemProps {
detail: string, detail: string,
action: () => void; action: () => void;
onDelete?: () => void; onDelete?: () => void;
active?: boolean;
disabled?: boolean; disabled?: boolean;
testId?: string; testId?: string;
custom?: boolean; custom?: boolean;
@ -36,6 +38,7 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
detail, detail,
action, action,
onDelete, onDelete,
active,
disabled, disabled,
testId, testId,
custom = false custom = false
@ -65,7 +68,7 @@ const IntegrationItem: React.FC<IntegrationItemProps> = ({
detail={detail} detail={detail}
hideActions={!disabled} hideActions={!disabled}
testId={testId} 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} onClick={handleClick}
/>; />;
}; };
@ -80,6 +83,9 @@ const BuiltInIntegrations: React.FC = () => {
const zapierDisabled = config.hostSettings?.limits?.customIntegrations?.disabled; 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 ( return (
<List titleSeparator={false}> <List titleSeparator={false}>
<IntegrationItem <IntegrationItem
@ -96,6 +102,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => { action={() => {
openModal('integrations/slack'); openModal('integrations/slack');
}} }}
active={slackUrl && slackUsername}
detail='A messaging app for teams' detail='A messaging app for teams'
icon={<SlackIcon className='h-8 w-8' />} icon={<SlackIcon className='h-8 w-8' />}
title='Slack' /> title='Slack' />
@ -104,6 +111,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => { action={() => {
openModal('integrations/amp'); openModal('integrations/amp');
}} }}
active={ampEnabled}
detail='Google Accelerated Mobile Pages' detail='Google Accelerated Mobile Pages'
icon={<AmpIcon className='h-8 w-8' />} icon={<AmpIcon className='h-8 w-8' />}
title='AMP' /> title='AMP' />
@ -112,6 +120,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => { action={() => {
openModal('integrations/unsplash'); openModal('integrations/unsplash');
}} }}
active={unsplashEnabled}
detail='Beautiful, free photos' detail='Beautiful, free photos'
icon={<UnsplashIcon className='h-8 w-8' />} icon={<UnsplashIcon className='h-8 w-8' />}
title='Unsplash' /> title='Unsplash' />
@ -120,6 +129,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => { action={() => {
openModal('integrations/firstpromoter'); openModal('integrations/firstpromoter');
}} }}
active={firstPromoterEnabled}
detail='Launch your member referral program' detail='Launch your member referral program'
icon={<FirstPromoterIcon className='h-8 w-8' />} icon={<FirstPromoterIcon className='h-8 w-8' />}
title='FirstPromoter' /> title='FirstPromoter' />
@ -128,6 +138,7 @@ const BuiltInIntegrations: React.FC = () => {
action={() => { action={() => {
openModal('integrations/pintura'); openModal('integrations/pintura');
}} }}
active={pinturaEnabled}
detail='Advanced image editing' icon= detail='Advanced image editing' icon=
{<PinturaIcon className='h-8 w-8' />} title {<PinturaIcon className='h-8 w-8' />} title
='Pintura' /> ='Pintura' />

View file

@ -90,7 +90,7 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
} else { } else {
showToast({ showToast({
type: 'pageError', 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.'
}); });
} }
}} }}

View file

@ -43,7 +43,7 @@ const SlackModal = NiceModal.create(() => {
} else { } else {
showToast({ showToast({
type: 'pageError', 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 { } else {
showToast({ showToast({
type: 'pageError', 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.'
}); });
} }
}} }}

View file

@ -67,7 +67,7 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
} else { } else {
showToast({ showToast({
type: 'pageError', 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.'
}); });
} }
}} }}

View file

@ -83,7 +83,7 @@ const AddNewsletterModal: React.FC<AddNewsletterModalProps> = () => {
} else { } else {
showToast({ showToast({
type: 'pageError', 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.'
}); });
} }
}} }}

View file

@ -20,6 +20,7 @@ import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
import useFeatureFlag from '../../../../hooks/useFeatureFlag'; import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useForm, {ErrorMessages} from '../../../../hooks/useForm'; import useForm, {ErrorMessages} from '../../../../hooks/useForm';
import useRouting from '../../../../hooks/useRouting'; import useRouting from '../../../../hooks/useRouting';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import validator from 'validator'; import validator from 'validator';
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '../../../../api/newsletters'; import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '../../../../api/newsletters';
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal'; import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
@ -44,6 +45,8 @@ const Sidebar: React.FC<{
const {mutateAsync: uploadImage} = useUploadImage(); const {mutateAsync: uploadImage} = useUploadImage();
const [selectedTab, setSelectedTab] = useState('generalSettings'); const [selectedTab, setSelectedTab] = useState('generalSettings');
const hasEmailCustomization = useFeatureFlag('emailCustomization'); const hasEmailCustomization = useFeatureFlag('emailCustomization');
const {localSettings} = useSettingGroup();
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
const replyToEmails = [ const replyToEmails = [
{label: `Newsletter address (${fullEmailAddress(newsletter.sender_email || 'noreply', siteData)})`, value: 'newsletter'}, {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})} /> <TextArea rows={2} title="Description" value={newsletter.description || ''} onChange={e => updateNewsletter({description: e.target.value})} />
</Form> </Form>
<Form className='mt-6' gap='sm' margins='lg' title='Email addresses'> <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 <TextField
error={Boolean(errors.sender_email)} error={Boolean(errors.sender_email)}
hint={errors.sender_email} hint={errors.sender_email}
placeholder="noreply@localhost" placeholder={fullEmailAddress(newsletter.sender_email || 'noreply', siteData)}
title="Sender email address" title="Sender email address"
value={newsletter.sender_email || ''} value={newsletter.sender_email || ''}
onBlur={validate} onBlur={validate}
@ -143,21 +146,21 @@ const Sidebar: React.FC<{
checked={newsletter.show_header_icon} checked={newsletter.show_header_icon}
direction="rtl" direction="rtl"
label='Publication icon' label='Publication icon'
labelStyle='value' labelStyle='heading'
onChange={e => updateNewsletter({show_header_icon: e.target.checked})} onChange={e => updateNewsletter({show_header_icon: e.target.checked})}
/>} />}
<Toggle <Toggle
checked={newsletter.show_header_title} checked={newsletter.show_header_title}
direction="rtl" direction="rtl"
label='Publication title' label='Publication title'
labelStyle='value' labelStyle='heading'
onChange={e => updateNewsletter({show_header_title: e.target.checked})} onChange={e => updateNewsletter({show_header_title: e.target.checked})}
/> />
<Toggle <Toggle
checked={newsletter.show_header_name} checked={newsletter.show_header_name}
direction="rtl" direction="rtl"
label='Newsletter name' label='Newsletter name'
labelStyle='value' labelStyle='heading'
onChange={e => updateNewsletter({show_header_name: e.target.checked})} onChange={e => updateNewsletter({show_header_name: e.target.checked})}
/> />
</ToggleGroup> </ToggleGroup>
@ -432,7 +435,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter}> = ({newsl
} else { } else {
showToast({ showToast({
type: 'pageError', 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.'
}); });
} }
}} }}

View file

@ -93,6 +93,7 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) =>
return <NewsletterPreviewContent return <NewsletterPreviewContent
authorPlaceholder={currentUser.name || currentUser.email} authorPlaceholder={currentUser.name || currentUser.email}
backgroundColor={colors.backgroundColor || '#ffffff'}
bodyFontCategory={newsletter.body_font_category} bodyFontCategory={newsletter.body_font_category}
footerContent={newsletter.footer_content} footerContent={newsletter.footer_content}
headerIcon={newsletter.show_header_icon ? icon : undefined} headerIcon={newsletter.show_header_icon ? icon : undefined}

View file

@ -124,7 +124,7 @@ const NewsletterPreviewContent: React.FC<{
</span> </span>
)} )}
</p> </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>
</div> </div>
)} )}

View file

@ -782,7 +782,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
if (error) { if (error) {
showToast({ showToast({
type: 'pageError', 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(''); setSaveState('');
return; return;

View file

@ -25,9 +25,9 @@ const AccountPage: React.FC<{
setValue(fullEmailAddress(settingValue, siteData!)); 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)} /> <TextField title='Support email address' value={value} onBlur={updateSupportAddress} onChange={e => setValue(e.target.value)} />
</Form>; </Form></div>;
}; };
export default AccountPage; export default AccountPage;

View file

@ -62,7 +62,7 @@ const LookAndFeel: React.FC<{
setUploadedIcon(undefined); setUploadedIcon(undefined);
}; };
return <Form marginTop> return <div className='mt-7'><Form>
<Toggle <Toggle
checked={Boolean(portalButton)} checked={Boolean(portalButton)}
label='Show portal button' label='Show portal button'
@ -116,7 +116,7 @@ const LookAndFeel: React.FC<{
onChange={e => updateSetting('portal_button_signup_text', e.target.value)} onChange={e => updateSetting('portal_button_signup_text', e.target.value)}
/> />
} }
</Form>; </Form></div>;
}; };
export default LookAndFeel; export default LookAndFeel;

View file

@ -1,7 +1,7 @@
import AccountPage from './AccountPage'; import AccountPage from './AccountPage';
import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal'; import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal';
import LookAndFeel from './LookAndFeel'; import LookAndFeel from './LookAndFeel';
import NiceModal, {useModal} from '@ebay/nice-modal-react'; import NiceModal from '@ebay/nice-modal-react';
import PortalPreview from './PortalPreview'; import PortalPreview from './PortalPreview';
import React, {useState} from 'react'; import React, {useState} from 'react';
import SignupOptions from './SignupOptions'; import SignupOptions from './SignupOptions';
@ -61,7 +61,6 @@ const Sidebar: React.FC<{
}; };
const PortalModal: React.FC = () => { const PortalModal: React.FC = () => {
const modal = useModal();
const {updateRoute} = useRouting(); const {updateRoute} = useRouting();
const [selectedPreviewTab, setSelectedPreviewTab] = useState('signup'); const [selectedPreviewTab, setSelectedPreviewTab] = useState('signup');
@ -152,7 +151,7 @@ const PortalModal: React.FC = () => {
{id: 'account', title: 'Account page'}, {id: 'account', title: 'Account page'},
{id: 'links', title: 'Links'} {id: 'links', title: 'Links'}
]; ];
let okLabel = 'Save & close'; let okLabel = 'Save';
if (saveState === 'saving') { if (saveState === 'saving') {
okLabel = 'Saving...'; okLabel = 'Saving...';
} else if (saveState === 'saved') { } else if (saveState === 'saved') {
@ -163,6 +162,7 @@ const PortalModal: React.FC = () => {
afterClose={() => { afterClose={() => {
updateRoute('portal'); updateRoute('portal');
}} }}
cancelLabel='Close'
deviceSelector={false} deviceSelector={false}
dirty={saveState === 'unsaved'} dirty={saveState === 'unsaved'}
okLabel={okLabel} okLabel={okLabel}
@ -176,8 +176,6 @@ const PortalModal: React.FC = () => {
onOk={async () => { onOk={async () => {
if (!Object.values(errors).filter(Boolean).length) { if (!Object.values(errors).filter(Boolean).length) {
await handleSave(); await handleSave();
updateRoute('portal');
modal.remove();
} }
}} }}
onSelectURL={onSelectURL} onSelectURL={onSelectURL}

View file

@ -83,7 +83,7 @@ const SignupOptions: React.FC<{
}); });
} }
return <Form marginTop> return <div className='mt-7'><Form>
<Toggle <Toggle
checked={Boolean(portalName)} checked={Boolean(portalName)}
disabled={isDisabled} disabled={isDisabled}
@ -141,7 +141,7 @@ const SignupOptions: React.FC<{
labelStyle='heading' labelStyle='heading'
onChange={e => updateSetting('portal_signup_checkbox_required', e.target.checked)} onChange={e => updateSetting('portal_signup_checkbox_required', e.target.checked)}
/>} />}
</Form>; </Form></div>;
}; };
export default SignupOptions; export default SignupOptions;

View file

@ -121,11 +121,16 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
if (Object.values(validators).filter(validator => validator()).length) { if (Object.values(validators).filter(validator => validator()).length) {
showToast({ showToast({
type: 'pageError', 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; return;
} }
if (saveState !== 'unsaved') {
updateRoute('tiers');
modal.remove();
}
if (await handleSave()) { if (await handleSave()) {
updateRoute('tiers'); updateRoute('tiers');
} }
@ -173,7 +178,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
placeholder='1' placeholder='1'
rightPlaceholder={`${formState.currency}/month`} rightPlaceholder={`${formState.currency}/month`}
title='Monthly price' title='Monthly price'
valueInCents={formState.monthly_price || 0} valueInCents={formState.monthly_price || ''}
hideTitle hideTitle
onBlur={() => validators.monthly_price()} onBlur={() => validators.monthly_price()}
onChange={price => updateForm(state => ({...state, monthly_price: price}))} onChange={price => updateForm(state => ({...state, monthly_price: price}))}
@ -184,7 +189,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
placeholder='10' placeholder='10'
rightPlaceholder={`${formState.currency}/year`} rightPlaceholder={`${formState.currency}/year`}
title='Yearly price' title='Yearly price'
valueInCents={formState.yearly_price || 0} valueInCents={formState.yearly_price || ''}
hideTitle hideTitle
onBlur={() => validators.yearly_price()} onBlur={() => validators.yearly_price()}
onChange={price => updateForm(state => ({...state, yearly_price: price}))} onChange={price => updateForm(state => ({...state, yearly_price: price}))}
@ -197,9 +202,9 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
</div> </div>
<TextField <TextField
disabled={!hasFreeTrial} disabled={!hasFreeTrial}
hint={<> hint={<div className='mt-1'>
Members will be subscribed at full price once the trial ends. <a href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a> 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' placeholder='0'
rightPlaceholder='days' rightPlaceholder='days'
title='Trial days' title='Trial days'

View file

@ -2,6 +2,7 @@ import Button from '../../../../admin-x-ds/global/Button';
import Heading from '../../../../admin-x-ds/global/Heading'; import Heading from '../../../../admin-x-ds/global/Heading';
import Icon from '../../../../admin-x-ds/global/Icon'; import Icon from '../../../../admin-x-ds/global/Icon';
import React, {useState} from 'react'; import React, {useState} from 'react';
import clsx from 'clsx';
import useSettingGroup from '../../../../hooks/useSettingGroup'; import useSettingGroup from '../../../../hooks/useSettingGroup';
import {TierFormState} from './TierDetailModal'; import {TierFormState} from './TierDetailModal';
import {currencyToDecimal, getSymbol} from '../../../../utils/currency'; import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
@ -13,12 +14,18 @@ interface TierDetailPreviewProps {
isFreeTier: boolean; isFreeTier: boolean;
} }
const TrialDaysLabel: React.FC<{trialDays: number}> = ({trialDays}) => { export const TrialDaysLabel: React.FC<{size?: 'sm' | 'md'; trialDays: number;}> = ({size = 'md', trialDays}) => {
if (!trialDays) { if (!trialDays) {
return null; 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 ( 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> <span className="absolute inset-0 block rounded-full bg-pink opacity-20"></span>
{trialDays} days free {trialDays} days free
</span> </span>

View file

@ -4,6 +4,7 @@ import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
import React from 'react'; import React from 'react';
import useRouting from '../../../../hooks/useRouting'; import useRouting from '../../../../hooks/useRouting';
import {Tier, useEditTier} from '../../../../api/tiers'; import {Tier, useEditTier} from '../../../../api/tiers';
import {TrialDaysLabel} from './TierDetailPreview';
import {currencyToDecimal, getSymbol} from '../../../../utils/currency'; import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
import {numberWithCommas} from '../../../../utils/helpers'; 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> <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>} {(tier.monthly_price && tier.monthly_price > 0) && <span className='text-sm text-grey-700'>/month</span>}
</div> </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'> <div className='mt-2 line-clamp-2 text-[1.4rem] font-medium'>
{tier.description || <span className='opacity-50'>No description</span>} {tier.description || <span className='opacity-50'>No description</span>}
</div> </div>

View file

@ -136,7 +136,7 @@ const AddRecommendationModal: React.FC<AddRecommendationModalProps> = ({recommen
} else { } else {
showToast({ showToast({
type: 'pageError', 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) { } catch (e) {

View file

@ -104,7 +104,7 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
} else { } else {
showToast({ showToast({
type: 'pageError', 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.'
}); });
} }
}} }}

View file

@ -70,7 +70,7 @@ const EditRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({
} else { } else {
showToast({ showToast({
type: 'pageError', 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.'
}); });
} }
}} }}

View file

@ -105,7 +105,7 @@ const useSettingGroup = ({onValidate}: {onValidate?: () => ErrorMessages} = {}):
} else { } else {
showToast({ showToast({
type: 'pageError', 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; return result;

View file

@ -297,12 +297,12 @@ export default class AdminXSettings extends Component {
justifyContent: 'center', justifyContent: 'center',
paddingBottom: '8vh' 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', width: '100px',
height: '100px' height: '100px'
}}> }}>
<source src="assets/videos/logo-loader.mp4" type="video/mp4" /> <source src="assets/videos/logo-loader.mp4" type="video/mp4" />
<div class="gh-loading-spinner"></div> <div className="gh-loading-spinner"></div>
</video> </video>
</div> </div>
); );