AdminX UI fixes (#18167)

refs. https://github.com/TryGhost/Product/issues/3349
This commit is contained in:
Peter Zimon 2023-09-15 19:35:16 +03:00 committed by GitHub
parent 37a9ffc63e
commit 3043c27d5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 176 additions and 155 deletions

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>laptop</title><path d="M21,14.25V4.5A1.5,1.5,0,0,0,19.5,3H4.5A1.5,1.5,0,0,0,3,4.5v9.75Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><path d="M23.121,18.891A1.5,1.5,0,0,1,21.75,21H2.25A1.5,1.5,0,0,1,.879,18.891L3,14.25H21Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><line x1="10.5" y1="18" x2="13.5" y2="18" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>Desktop</title><path d="M21,14.25V4.5A1.5,1.5,0,0,0,19.5,3H4.5A1.5,1.5,0,0,0,3,4.5v9.75Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><path d="M23.121,18.891A1.5,1.5,0,0,1,21.75,21H2.25A1.5,1.5,0,0,1,.879,18.891L3,14.25H21Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><line x1="10.5" y1="18" x2="13.5" y2="18" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></svg>

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 636 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><rect x="5.25" y="0.75" width="13.5" height="22.5" rx="3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></rect><line x1="5.25" y1="17.75" x2="18.75" y2="17.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Mobile</title><g><rect x="5.25" y="0.75" width="13.5" height="22.5" rx="3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></rect><line x1="5.25" y1="17.75" x2="18.75" y2="17.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></g></svg>

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 419 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5"><defs></defs><title>picture-sun</title><path d="M2.25.75h19.5s1.5 0 1.5 1.5v19.5s0 1.5-1.5 1.5H2.25s-1.5 0-1.5-1.5V2.25s0-1.5 1.5-1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M13.5 7.5a3 3 0 1 0 6 0 3 3 0 1 0-6 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3.961 14.959a8.194 8.194 0 0 1 11.694 4.149" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14.382 16.918a4.449 4.449 0 0 1 5.851-.125" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>

After

Width:  |  Height:  |  Size: 729 B

View File

@ -1,5 +1,6 @@
import Icon from './Icon';
import React, {HTMLProps} from 'react';
import clsx from 'clsx';
import {LoadingIndicator, LoadingIndicatorColor, LoadingIndicatorSize} from './LoadingIndicator';
export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline';
@ -15,6 +16,7 @@ export interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, 'label'
color?: ButtonColor;
fullWidth?: boolean;
link?: boolean;
linkWithPadding?: boolean;
disabled?: boolean;
unstyled?: boolean;
className?: string;
@ -34,6 +36,7 @@ const Button: React.FC<ButtonProps> = ({
color = 'clear',
fullWidth,
link,
linkWithPadding = false,
disabled,
unstyled = false,
className = '',
@ -47,50 +50,74 @@ const Button: React.FC<ButtonProps> = ({
color = 'clear';
}
let styles = '';
if (!unstyled) {
styles += ' transition whitespace-nowrap flex items-center justify-center rounded-sm text-sm';
styles += ((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? ' font-bold' : ' font-semibold';
styles += !link ? `${size === 'sm' ? ' px-3 h-7 ' : ' px-4 h-[34px] '}` : '';
className = clsx(
'flex items-center justify-center whitespace-nowrap rounded-sm text-sm transition',
((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? 'font-bold' : 'font-semibold',
!link ? `${size === 'sm' ? ' h-7 px-3 ' : ' h-[34px] px-4 '}` : '',
(link && linkWithPadding) && '-m-1 p-1',
className
);
switch (color) {
case 'black':
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`;
className = clsx(
link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`,
className
);
loadingIndicatorColor = 'light';
break;
case 'grey':
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`;
className = clsx(
link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`,
className
);
loadingIndicatorColor = 'dark';
break;
case 'green':
styles += link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`;
className = clsx(
link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`,
className
);
loadingIndicatorColor = 'light';
break;
case 'red':
styles += link ? ' text-red hover:text-red-400' : ` bg-red text-white ${!disabled && 'hover:bg-red-400'}`;
className = clsx(
link ? 'text-red hover:text-red-400' : `bg-red text-white ${!disabled && 'hover:bg-red-400'}`,
className
);
loadingIndicatorColor = 'light';
break;
case 'white':
styles += link ? ' text-white hover:text-white dark:text-black dark:hover:text-grey-800' : ` bg-white dark:bg-black text-black dark:text-white`;
className = clsx(
link ? 'text-white hover:text-white dark:text-black dark:hover:text-grey-800' : `bg-white text-black dark:bg-black dark:text-white`,
className
);
loadingIndicatorColor = 'dark';
break;
case 'outline':
styles += link ? ' text-black dark:text-white hover:text-grey-800' : `text-black border border-grey-300 bg-transparent dark:border-grey-800 dark:text-white ${!disabled && 'hover:!border-black dark:hover:!border-white'}`;
className = clsx(
link ? 'text-black hover:text-grey-800 dark:text-white' : `border border-grey-300 bg-transparent text-black dark:border-grey-800 dark:text-white ${!disabled && 'hover:!border-black dark:hover:!border-white'}`,
className
);
loadingIndicatorColor = 'dark';
break;
default:
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`;
className = clsx(
link ? ' text-black hover:text-grey-800 dark:text-white' : ` text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`,
className
);
loadingIndicatorColor = 'dark';
break;
}
styles += (fullWidth && !link) ? ' w-full' : '';
styles += (disabled) ? ' opacity-40' : ' cursor-pointer';
className = clsx(
(fullWidth && !link) && ' w-full',
disabled ? 'opacity-40' : 'cursor-pointer',
className
);
}
styles += ` ${className}`;
const iconClasses = label && icon && !hideLabel ? 'mr-1.5' : '';
let labelClasses = '';
@ -102,8 +129,8 @@ const Button: React.FC<ButtonProps> = ({
<span className={labelClasses}>{label}</span>
{loading && <div className='absolute flex'><LoadingIndicator color={loadingIndicatorColor} size={size}/><span className='sr-only'>Loading...</span></div>}
</>;
const buttonElement = React.createElement(tag, {className: styles,
const buttonElement = React.createElement(tag, {className: className,
disabled: disabled,
type: 'button',
onClick: onClick,

View File

@ -6,14 +6,15 @@ import {ButtonProps} from './Button';
interface ButtonGroupProps {
buttons: Array<ButtonProps>;
link?: boolean;
linkWithPadding?: boolean;
className?: string;
}
const ButtonGroup: React.FC<ButtonGroupProps> = ({buttons, link, className}) => {
const ButtonGroup: React.FC<ButtonGroupProps> = ({buttons, link, linkWithPadding, className}) => {
return (
<div className={`flex items-center ${link ? 'gap-5' : 'gap-3'} ${className}`}>
{buttons.map(({key, ...props}) => (
<Button key={key} link={link} {...props} />
<Button key={key} link={link} linkWithPadding={linkWithPadding} {...props} />
))}
</div>
);

View File

@ -6,8 +6,8 @@ interface DesktopChromeProps {
const DesktopChrome: React.FC<DesktopChromeProps & React.HTMLAttributes<HTMLDivElement>> = ({children, ...props}) => {
return (
<div className='flex h-full w-full flex-col px-5 pb-5' {...props}>
<div className="h-full w-full overflow-hidden rounded-[4px] shadow-sm">
<div className='flex h-full w-full flex-col px-5' {...props}>
<div className="h-full w-full overflow-hidden rounded-t-[4px] shadow-sm">
{children}
</div>
</div>

View File

@ -163,7 +163,7 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
{customHeader ? customHeader :
<SettingGroupHeader description={description} title={title!}>
{customButtons ? customButtons :
(onEditingChange && <ButtonGroup buttons={isEditing ? editButtons : viewButtons} link={true} />)}
(onEditingChange && <ButtonGroup buttons={isEditing ? editButtons : viewButtons} link linkWithPadding />)}
</SettingGroupHeader>
}
{children}

View File

@ -6,7 +6,7 @@ interface Props {
}
const SettingSectionHeader: React.FC<Props> = ({title, sticky = false}) => {
let styles = 'pb-4 text-2xs font-semibold uppercase tracking-wider text-grey-700 z-10 ';
let styles = 'pb-4 text-2xs font-semibold uppercase tracking-wider text-grey-700 z-20 ';
if (sticky) {
styles += ' sticky top-0 -mt-4 pt-4 bg-white dark:bg-black';
}

View File

@ -63,8 +63,8 @@ const Sidebar: React.FC = () => {
</SettingNavSection>
<SettingNavSection keywords={Object.values(membershipSearchKeywords).flat()} title="Membership">
<SettingNavItem keywords={membershipSearchKeywords.portal} navid='portal' title="Portal" onClick={handleSectionClick} />
<SettingNavItem keywords={membershipSearchKeywords.access} navid='access' title="Access" onClick={handleSectionClick} />
<SettingNavItem keywords={membershipSearchKeywords.portal} navid='portal' title="Portal" onClick={handleSectionClick} />
<SettingNavItem keywords={membershipSearchKeywords.tiers} navid='tiers' title="Tiers" onClick={handleSectionClick} />
{hasTipsAndDonations && <SettingNavItem keywords={membershipSearchKeywords.tips} navid='tips-or-donations' title="Tips or donations" onClick={handleSectionClick} />}
<SettingNavItem keywords={membershipSearchKeywords.embedSignupForm} navid='embed-signup-form' title="Embeddable signup form" onClick={handleSectionClick} />

View File

@ -11,7 +11,7 @@ const History: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<SettingGroup
customButtons={<Button color='green' label='View history' link onClick={openHistoryModal}/>}
customButtons={<Button color='green' label='View history' link linkWithPadding onClick={openHistoryModal}/>}
description="View system event log"
keywords={keywords}
navid='history'

View File

@ -210,7 +210,7 @@ const Integrations: React.FC<{ keywords: string[] }> = ({keywords}) => {
] as const;
const buttons = (
<Button className='hidden md:!visible md:!block' color='green' label='Add custom integration' link={true} onClick={() => {
<Button className='hidden md:!visible md:!block' color='green' label='Add custom integration' link linkWithPadding onClick={() => {
updateRoute('integrations/add');
setSelectedTab('custom');
}} />

View File

@ -41,10 +41,10 @@ const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => {
<SettingGroupHeader description='This is a testing ground for new or experimental features. They may change, break or inexplicably disappear at any time.' title='Labs' />
{
!isOpen ?
<Button color='green' label='Open' link onClick={() => {
<Button color='green' label='Open' link linkWithPadding onClick={() => {
setIsOpen(true);
}} /> :
<Button color='green' label='Close' link onClick={() => {
<Button color='green' label='Close' link linkWithPadding onClick={() => {
setIsOpen(false);
}} />
}

View File

@ -1,4 +1,5 @@
import Button from '../../../../admin-x-ds/global/Button';
import Heading from '../../../../admin-x-ds/global/Heading';
import React, {ReactNode, useState} from 'react';
import clsx from 'clsx';
@ -19,29 +20,28 @@ const APIKeyField: React.FC<APIKeyFieldProps> = ({label, text = '', hint, onRege
};
const containerClasses = clsx(
'group/api-keys relative mb-3 w-full overflow-hidden rounded py-1 text-sm hover:bg-grey-50 dark:hover:bg-black md:mb-0',
label ? 'md:p-1' : 'md:px-0'
'group/api-keys relative -mt-1 mb-1 w-full overflow-hidden border-b border-transparent py-2 hover:border-grey-300 dark:hover:border-grey-600'
);
return <>
{label && <div className='p-0 pr-4 text-sm text-grey-600 md:py-1'>{label}</div>}
return <div className='mb-3 grid grid-cols-1'>
{label && <Heading level={6} grey>{label}</Heading>}
<div className={containerClasses}>
{text}
<span>{text}</span>
{hint}
<div className='visible absolute right-0 top-[50%] flex translate-y-[-50%] gap-1 bg-white pl-1 text-sm group-hover/api-keys:visible dark:bg-black md:invisible'>
{onRegenerate && <Button color='outline' label='Regenerate' size='sm' onClick={onRegenerate} />}
<Button color='outline' label={copied ? 'Copied' : 'Copy'} size='sm' onClick={copyText} />
<div className='visible absolute right-0 top-1/2 flex translate-y-[-50%] gap-1 pl-1 text-sm group-hover/api-keys:visible dark:bg-black md:invisible'>
{onRegenerate && <Button className='!bg-white' color='outline' label='Regenerate' size='sm' onClick={onRegenerate} />}
<Button className='!bg-white' color='outline' label={copied ? 'Copied' : 'Copy'} size='sm' onClick={copyText} />
</div>
</div>
</>;
</div>;
};
const APIKeys: React.FC<{hasLabel?: boolean; keys: APIKeyFieldProps[];}> = ({hasLabel = true, keys}) => {
const APIKeys: React.FC<{hasLabel?: boolean; keys: APIKeyFieldProps[];}> = ({keys}) => {
return (
<div className={hasLabel ? 'grid grid-cols-1 md:grid-cols-[max-content_1fr]' : ''}>
<div>
{keys.map(key => <APIKeyField key={key.label} {...key} />)}
</div>
);
};
export default APIKeys;
export default APIKeys;

View File

@ -123,26 +123,24 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
onKeyDown={() => clearError('name')}
/>
<TextField title='Description' value={formState.description || ''} onChange={e => updateForm(state => ({...state, description: e.target.value}))} />
<div>
<APIKeys keys={[
{
label: 'Content API key',
text: contentApiKey?.secret,
hint: contentKeyRegenerated ? <div className='text-green'>Content API Key was successfully regenerated</div> : undefined,
onRegenerate: () => contentApiKey && handleRegenerate(contentApiKey, setContentKeyRegenerated)
},
{
label: 'Admin API key',
text: adminApiKey?.secret,
hint: adminKeyRegenerated ? <div className='text-green'>Admin API Key was successfully regenerated</div> : undefined,
onRegenerate: () => adminApiKey && handleRegenerate(adminApiKey, setAdminKeyRegenerated)
},
{
label: 'API URL',
text: window.location.origin + getGhostPaths().subdir
}
]} />
</div>
<APIKeys keys={[
{
label: 'Content API key',
text: contentApiKey?.secret,
hint: contentKeyRegenerated ? <div className='text-green'>Content API Key was successfully regenerated</div> : undefined,
onRegenerate: () => contentApiKey && handleRegenerate(contentApiKey, setContentKeyRegenerated)
},
{
label: 'Admin API key',
text: adminApiKey?.secret,
hint: adminKeyRegenerated ? <div className='text-green'>Admin API Key was successfully regenerated</div> : undefined,
onRegenerate: () => adminApiKey && handleRegenerate(adminApiKey, setAdminKeyRegenerated)
},
{
label: 'API URL',
text: window.location.origin + getGhostPaths().subdir
}
]} />
</Form>
</div>
</div>

View File

@ -14,11 +14,11 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
extra
}) => {
return (
<div className='flex w-full flex-col gap-4 md:flex-row'>
<div className='-mx-8 -mt-8 flex flex-col gap-4 bg-grey-75 p-8 md:flex-row'>
<div className='h-14 w-14'>{icon}</div>
<div className='flex min-w-0 flex-1 flex-col'>
<div className='mt-1.5 flex min-w-0 flex-1 flex-col'>
<h3>{title}</h3>
<div className='text-grey-600'>{detail}</div>
<div>{detail}</div>
{extra && (
<div className='mt-4'>{extra}</div>
)}

View File

@ -70,10 +70,25 @@ const ZapierModal = NiceModal.create(() => {
updateRoute('integrations');
}}
cancelLabel=''
footer={
<div className='mx-8 flex w-full items-center justify-between'>
<a
className='mt-1 self-baseline text-sm font-bold'
href='https://zapier.com/apps/ghost/integrations?utm_medium=partner_api&utm_source=widget&utm_campaign=Widget'
rel='noopener noreferrer'
target='_blank'>
View more Ghost integrations powered by <span><Logo className='relative top-[-2px] inline-block h-6' /></span>
</a>
<Button color='black' label='Close' onClick={() => {
modal.remove();
}} />
</div>
}
okColor='black'
okLabel='Close'
testId='zapier-modal'
title=''
stickyFooter
onOk={() => {
updateRoute('integrations');
modal.remove();
@ -81,7 +96,7 @@ const ZapierModal = NiceModal.create(() => {
>
<IntegrationHeader
detail='Automation for your favorite apps'
extra={<APIKeys keys={[
extra={<div className='-mb-4 mt-1'><APIKeys keys={[
{
label: 'Admin API key',
text: adminApiKey?.secret,
@ -89,17 +104,17 @@ const ZapierModal = NiceModal.create(() => {
onRegenerate: handleRegenerate
},
{label: 'API URL', text: window.location.origin + getGhostPaths().subdir}
]} />}
]} /></div>}
icon={<Icon className='h-14 w-14' />}
title='Zapier'
/>
<List className='mt-6'>
<List className='-mb-8'>
{zapierTemplates.map(template => (
<ListItem
action={<Button className='whitespace-nowrap text-sm font-semibold text-[#FF4A00]' href={template.url} label='Use this Zap' tag='a' target='_blank' link unstyled />}
bgOnHover={false}
className='flex items-center gap-3 py-2'
className='flex items-center gap-3 py-2 pl-3'
title={
<div className='flex flex-col gap-4 md:flex-row md:items-center'>
<div className='flex shrink-0 flex-nowrap items-center gap-2'>
@ -114,16 +129,6 @@ const ZapierModal = NiceModal.create(() => {
/>
))}
</List>
<div className='mt-6'>
<a
className='mt-6 self-baseline text-sm font-bold'
href='https://zapier.com/apps/ghost/integrations?utm_medium=partner_api&utm_source=widget&utm_campaign=Widget'
rel='noopener noreferrer'
target='_blank'>
View more Ghost integrations powered by <span><Logo className='relative top-[-2px] inline-block h-6' /></span>
</a>
</div>
</Modal>
);
});

View File

@ -19,12 +19,12 @@ const EmailSettings: React.FC = () => {
const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string];
return (
<SettingSection keywords={Object.values(searchKeywords).flat()} title='Email newsletters'>
<SettingSection keywords={Object.values(searchKeywords).flat()} title='Email newsletter'>
<EnableNewsletters keywords={searchKeywords.enableNewsletters} />
{newslettersEnabled !== 'disabled' && (
<>
<Newsletters keywords={searchKeywords.newsletters} />
<DefaultRecipients keywords={searchKeywords.defaultRecipients} />
<Newsletters keywords={searchKeywords.newsletters} />
{!config.mailgunIsConfigured && <MailGun keywords={searchKeywords.mailgun} />}
</>
)}

View File

@ -15,7 +15,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {data: {newsletters} = {}} = useBrowseNewsletters();
const buttons = (
<Button color='green' label='Add newsletter' link={true} onClick={() => {
<Button color='green' label='Add newsletter' link linkWithPadding onClick={() => {
openNewsletterModal();
}} />
);

View File

@ -11,7 +11,7 @@ import NewsletterPreview from './NewsletterPreview';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import Select, {SelectOption} from '../../../../admin-x-ds/global/form/Select';
import StickyFooter from '../../../../admin-x-ds/global/StickyFooter';
import Separator from '../../../../admin-x-ds/global/Separator';
import TabView, {Tab} from '../../../../admin-x-ds/global/TabView';
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
import TextField from '../../../../admin-x-ds/global/form/TextField';
@ -110,6 +110,26 @@ const Sidebar: React.FC<{
onChange={e => updateNewsletter({subscribe_on_signup: e.target.checked})}
/>
</Form>
<Separator />
<div className='my-5 flex w-full items-start'>
<span>
<Icon className='mr-2 mt-[-1px]' colorClass='text-red' name='heart'/>
</span>
<Form marginBottom={false}>
<Toggle
checked={newsletter.show_badge}
direction='rtl'
label={
<div className='flex flex-col gap-0.5'>
<span className='text-sm md:text-base'>Promote independent publishing</span>
<span className='text-[11px] leading-tight text-grey-700 md:text-xs md:leading-tight'>Show youre a part of the indie publishing movement with a small badge in the footer</span>
</div>
}
labelStyle='value'
onChange={e => updateNewsletter({show_badge: e.target.checked})}
/>
</Form>
</div>
</>
},
{
@ -136,9 +156,9 @@ const Sidebar: React.FC<{
updateNewsletter({header_image: imageUrl});
}}
>
Upload header image
<Icon colorClass='text-grey-700 dark:text-grey-300' name='picture' />
</ImageUpload>
<Hint>Optional, recommended size 1200x600</Hint>
<Hint>1200x600, optional</Hint>
</div>
</div>
<ToggleGroup>
@ -146,21 +166,18 @@ const Sidebar: React.FC<{
checked={newsletter.show_header_icon}
direction="rtl"
label='Publication icon'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_icon: e.target.checked})}
/>}
<Toggle
checked={newsletter.show_header_title}
direction="rtl"
label='Publication title'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_title: e.target.checked})}
/>
<Toggle
checked={newsletter.show_header_name}
direction="rtl"
label='Newsletter name'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_name: e.target.checked})}
/>
</ToggleGroup>
@ -292,28 +309,24 @@ const Sidebar: React.FC<{
checked={newsletter.feedback_enabled}
direction="rtl"
label='Ask your readers for feedback'
labelStyle='heading'
onChange={e => updateNewsletter({feedback_enabled: e.target.checked})}
/>
<Toggle
checked={newsletter.show_comment_cta}
direction="rtl"
label='Add a link to your comments'
labelStyle='heading'
onChange={e => updateNewsletter({show_comment_cta: e.target.checked})}
/>
<Toggle
checked={newsletter.show_latest_posts}
direction="rtl"
label='Share your latest posts'
labelStyle='heading'
onChange={e => updateNewsletter({show_latest_posts: e.target.checked})}
/>
<Toggle
checked={newsletter.show_subscription_details}
direction="rtl"
label='Show subscription details'
labelStyle='heading'
onChange={e => updateNewsletter({show_subscription_details: e.target.checked})}
/>
</ToggleGroup>
@ -336,31 +349,10 @@ const Sidebar: React.FC<{
};
return (
<div className='flex h-full flex-col justify-between'>
<div className='flex flex-col'>
<div className='px-7 pb-7 pt-5'>
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={handleTabChange} />
</div>
<StickyFooter height={96}>
<div className='flex w-full items-start px-7'>
<span>
<Icon className='mr-2 mt-[-1px]' colorClass='text-red' name='heart'/>
</span>
<Form marginBottom={false}>
<Toggle
checked={newsletter.show_badge}
direction='rtl'
label={
<div className='flex flex-col gap-0.5'>
<span className='text-sm md:text-base'>Promote independent publishing</span>
<span className='text-[11px] leading-tight text-grey-700 md:text-xs md:leading-tight'>Show youre a part of the indie publishing movement with a small badge in the footer</span>
</div>
}
labelStyle='value'
onChange={e => updateNewsletter({show_badge: e.target.checked})}
/>
</Form>
</div>
</StickyFooter>
</div>
);
};

View File

@ -10,6 +10,7 @@ import TableRow from '../../../../admin-x-ds/global/TableRow';
import useRouting from '../../../../hooks/useRouting';
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
import {Newsletter, useEditNewsletter} from '../../../../api/newsletters';
import {numberWithCommas} from '../../../../utils/helpers';
interface NewslettersListProps {
newsletters: Newsletter[]
@ -21,7 +22,7 @@ const NewsletterItem: React.FC<{newsletter: Newsletter, onlyOne: boolean}> = ({n
const limiter = useLimiter();
const action = newsletter.status === 'active' ? (
<Button color='green' disabled={onlyOne} label='Archive' link onClick={() => {
<Button color='red' disabled={onlyOne} label='Archive' link onClick={() => {
NiceModal.show(ConfirmationModal, {
title: 'Archive newsletter',
prompt: <>
@ -78,14 +79,14 @@ const NewsletterItem: React.FC<{newsletter: Newsletter, onlyOne: boolean}> = ({n
</TableCell>
<TableCell className='hidden md:!visible md:!table-cell' onClick={showDetails}>
<div className={`flex grow flex-col`}>
<span>{newsletter.count?.active_members}</span>
<span>{numberWithCommas(newsletter.count?.active_members || 0) }</span>
<span className='whitespace-nowrap text-xs text-grey-700'>Subscribers</span>
</div>
</TableCell>
<TableCell className='hidden md:!visible md:!table-cell' onClick={showDetails}>
<div className={`flex grow flex-col`}>
<span>{newsletter.count?.posts}</span>
<span className='whitespace-nowrap text-xs text-grey-700'>Posts sent</span>
<span>{numberWithCommas(newsletter.count?.posts || 0)}</span>
<span className='whitespace-nowrap text-xs text-grey-700'>Delivered</span>
</div>
</TableCell>
</TableRow>

View File

@ -63,10 +63,10 @@ const TimeZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
<SettingGroupContent values={[
{
key: 'site-timezone',
value: publicationTimezone,
hint: (
<Hint timezone={publicationTimezone} />
)
value: <div className='flex flex-col'>
{publicationTimezone}
<span className='text-sm'><Hint timezone={publicationTimezone} /></span>
</div>
}
]} />
);

View File

@ -208,7 +208,7 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords,
};
const buttons = (
<Button color='green' label='Invite people' link={true} onClick={() => {
<Button color='green' label='Invite people' link={true} linkWithPadding onClick={() => {
showInviteModal();
}} />
);

View File

@ -53,8 +53,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
<SettingGroupContent columns={2}>
<Toggle
checked={trackEmailOpens}
direction='rtl'
hint='Record when a member opens an email'
label='Newsletter opens'
onChange={(e) => {
handleToggleChange('email_track_opens', e);
@ -62,8 +60,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
/>
<Toggle
checked={trackEmailClicks}
direction='rtl'
hint='Record when a member clicks on any link in an email'
label='Newsletter clicks'
onChange={(e) => {
handleToggleChange('email_track_clicks', e);
@ -71,8 +67,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
/>
<Toggle
checked={trackMemberSources}
direction='rtl'
hint='Track the traffic sources and posts that drive the most member growth'
label='Member sources'
onChange={(e) => {
handleToggleChange('members_track_sources', e);
@ -80,8 +74,6 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
/>
<Toggle
checked={outboundLinkTagging}
direction='rtl'
hint='Make it easier for other sites to track the traffic you send them in their analytics'
label='Outbound link tagging'
onChange={(e) => {
handleToggleChange('outbound_link_tagging', e);
@ -93,21 +85,21 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<SettingGroup
description='Decide what data you collect from your members'
hideEditButton={true}
isEditing={isEditing}
keywords={keywords}
navid='analytics'
saveState={saveState}
testId='analytics'
title='Analytics'
hideEditButton
onCancel={handleCancel}
onEditingChange={handleEditingChange}
onSave={handleSave}
>
{inputs}
<div className='mt-1'>
<Button color='green' label='Export post analytics' link={true} onClick={exportPosts} />
<div className='text-xs text-grey-700 dark:text-grey-600'>Download the data from your last 1,000 posts</div>
<div className='items-center-mt-1 flex justify-between'>
<Button color='green' label='Export' link linkWithPadding onClick={exportPosts} />
<a className='text-sm text-green' href="https://ghost.org/help/post-analytics/" rel="noopener noreferrer" target="_blank">Learn about analytics</a>
</div>
</SettingGroup>
);

View File

@ -25,8 +25,8 @@ const MembershipSettings: React.FC = () => {
return (
<SettingSection keywords={Object.values(searchKeywords).flat()} title='Membership'>
<Portal keywords={searchKeywords.portal} />
<Access keywords={searchKeywords.access} />
<Portal keywords={searchKeywords.portal} />
<Tiers keywords={searchKeywords.tiers} />
{hasTipsAndDonations && <TipsOrDonations keywords={searchKeywords.tips} />}
<EmbedSignupForm keywords={searchKeywords.embedSignupForm} />

View File

@ -17,7 +17,7 @@ const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<SettingGroup
customButtons={<Button color='green' disabled={membersSignupAccess === 'none'} label='Customize' link onClick={openPreviewModal}/>}
customButtons={<Button color='green' disabled={membersSignupAccess === 'none'} label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
description="Customize members modal signup flow"
keywords={keywords}
navid='portal'

View File

@ -185,7 +185,7 @@ const Connected: React.FC<{onClose?: () => void}> = ({onClose}) => {
return (
<section>
<div className='flex items-center justify-between'>
<Button className='dark:text-white' disabled={isFetchingMembers} icon='link-broken' iconColorClass='dark:text-white' label='Disconnect' link onClick={openDisconnectStripeModal} />
<Button color='red' disabled={isFetchingMembers} icon='link-broken' iconColorClass='text-red' label='Disconnect' link onClick={openDisconnectStripeModal} />
<Button icon='close' iconColorClass='dark:text-white' label='Close' size='sm' hideLabel link onClick={onClose} />
</div>
<div className='my-20 flex flex-col items-center'>

View File

@ -11,7 +11,7 @@ const AnnouncementBar: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<SettingGroup
customButtons={<Button color='green' label='Customize' link onClick={openModal}/>}
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openModal}/>}
description="Highlight important updates or offers"
keywords={keywords}
navid='announcement-bar'

View File

@ -11,7 +11,7 @@ const DesignSetting: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<SettingGroup
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
description="Customize the look and feel of your site"
keywords={keywords}
navid='design'

View File

@ -11,7 +11,7 @@ const Navigation: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<SettingGroup
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
description="Set up primary and secondary menus"
keywords={keywords}
navid='navigation'

View File

@ -64,12 +64,12 @@ const NavigationModal = NiceModal.create(() => {
tabs={[
{
id: 'primary-nav',
title: 'Primary navigation',
title: 'Primary',
contents: <NavigationEditForm baseUrl={siteData!.url} navigation={navigation} />
},
{
id: 'secondary-nav',
title: 'Secondary navigation',
title: 'Secondary',
contents: <NavigationEditForm baseUrl={siteData!.url} navigation={secondaryNavigation} />
}
]}

View File

@ -1,4 +1,5 @@
import Button from '../../../../admin-x-ds/global/Button';
import Icon from '../../../../admin-x-ds/global/Icon';
import NavigationItemEditor from './NavigationItemEditor';
import React from 'react';
import SortableList from '../../../../admin-x-ds/global/SortableList';
@ -23,16 +24,19 @@ const NavigationEditForm: React.FC<{
)}
onMove={navigation.moveItem}
/>
<NavigationItemEditor
action={<Button className='self-center' color='green' data-testid="add-button" icon="add" iconColorClass='text-white' size='sm' onClick={navigation.addItem} />}
baseUrl={baseUrl}
className="mt-1 pl-7"
clearError={key => navigation.clearError(navigation.newItem.id, key)}
data-testid="new-navigation-item"
item={navigation.newItem}
labelPlaceholder="New item label"
updateItem={navigation.setNewItem}
/>
<div className='flex items-center gap-3'>
<Icon colorClass='text-grey-300 dark:text-grey-900 mt-1' name='add' size='sm' />
<NavigationItemEditor
action={<Button className='mx-2 self-center rounded bg-green p-1' data-testid="add-button" icon="add" iconColorClass='text-white' size='sm' unstyled onClick={navigation.addItem} />}
baseUrl={baseUrl}
className="mt-1"
clearError={key => navigation.clearError(navigation.newItem.id, key)}
data-testid="new-navigation-item"
item={navigation.newItem}
labelPlaceholder="New item label"
updateItem={navigation.setNewItem}
/>
</div>
</div>;
};

View File

@ -49,7 +49,7 @@ test.describe('Analytics settings', async () => {
const section = page.getByTestId('analytics');
await section.getByRole('button', {name: 'Export post analytics'}).click();
await section.getByRole('button', {name: 'Export'}).click();
const hasDownloadUrl = lastApiRequests.postsExport?.url?.includes('/posts/export/?limit=1000');
expect(hasDownloadUrl).toBe(true);