2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

AdminX responsive design updates (#17979)

refs. https://github.com/TryGhost/Product/issues/3349

- A lot of pieces in AdminX missed proper handling of non-desktop devices
This commit is contained in:
Peter Zimon 2023-09-07 14:23:26 +03:00 committed by GitHub
parent 3d9f22c13a
commit 78e2cb0c28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 220 additions and 150 deletions

View file

@ -42,23 +42,23 @@ function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate}:
> >
<Toaster /> <Toaster />
<NiceModal.Provider> <NiceModal.Provider>
<div className='fixed left-6 top-4 z-20'> <div className='relative z-20 px-6 py-4 tablet:fixed'>
<ExitSettingsButton /> <ExitSettingsButton />
</div> </div>
{/* Main container */} {/* Main container */}
<div className="mx-auto flex max-w-[1080px] flex-col px-[5vmin] py-[12vmin] md:flex-row md:items-start md:gap-x-10 md:py-[8vmin]" id="admin-x-settings-content"> <div className="mx-auto flex max-w-[1080px] flex-col px-[5vmin] py-[12vmin] tablet:flex-row tablet:items-start tablet:gap-x-10 tablet:py-[8vmin]" id="admin-x-settings-content">
{/* Sidebar */} {/* Sidebar */}
<div className="relative z-20 min-w-[260px] grow-0 md:fixed md:top-[8vmin] md:basis-[260px]"> <div className="sticky top-[-42px] z-20 min-w-[260px] grow-0 md:top-[-52px] tablet:fixed tablet:top-[8vmin] tablet:basis-[260px]">
<div className='h-[84px]'> <div className='h-[84px]'>
<Heading>Settings</Heading> <Heading>Settings</Heading>
</div> </div>
<div className="relative mt-[-32px] w-[260px] overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:block after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-['']"> <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-[''] tablet:w-[260px] tablet:after:!visible tablet:after:!block">
<Sidebar /> <Sidebar />
</div> </div>
</div> </div>
<div className="relative flex-auto pt-[3vmin] md:ml-[300px] md:pt-[85px]"> <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] h-[80px] bg-gradient-to-t from-transparent to-white to-60%'></div> <div className='pointer-events-none fixed inset-x-0 top-0 z-[5] h-[80px] bg-gradient-to-t from-transparent to-white to-60%'></div>
<Settings /> <Settings />
</div> </div>

View file

@ -42,7 +42,7 @@ const Avatar: React.FC<AvatarProps> = ({image, label, labelColor, bgColor, size,
if (image) { if (image) {
return ( return (
<img alt="" className={`inline-flex items-center justify-center rounded-full object-cover font-semibold ${avatarSize} ${className && className}`} src={image}/> <img alt="" className={`inline-flex shrink-0 items-center justify-center rounded-full object-cover font-semibold ${avatarSize} ${className && className}`} src={image}/>
); );
} else if (label) { } else if (label) {
return ( return (

View file

@ -78,7 +78,7 @@ const Button: React.FC<ButtonProps> = ({
styles += ` ${className}`; styles += ` ${className}`;
const iconClasses = label && icon ? 'mr-1.5' : ''; const iconClasses = label && icon && !hideLabel ? 'mr-1.5' : '';
const buttonChildren = <> const buttonChildren = <>
{icon && <Icon className={iconClasses} colorClass={iconColorClass} name={icon} size={size === 'sm' ? 'sm' : 'md'} />} {icon && <Icon className={iconClasses} colorClass={iconColorClass} name={icon} size={size === 'sm' ? 'sm' : 'md'} />}

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import Separator from './Separator'; import Separator from './Separator';
import clsx from 'clsx';
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
@ -41,7 +42,7 @@ export const Heading6Styles = 'text-2xs font-semibold uppercase tracking-wider';
export const Heading6StylesGrey = 'text-2xs font-semibold uppercase tracking-wider text-grey-800'; export const Heading6StylesGrey = 'text-2xs font-semibold uppercase tracking-wider text-grey-800';
const Heading: React.FC<Heading1to5Props | Heading6Props | HeadingLabelProps> = ({ const Heading: React.FC<Heading1to5Props | Heading6Props | HeadingLabelProps> = ({
level, level = 1,
children, children,
styles = '', styles = '',
grey = true, grey = true,
@ -50,14 +51,37 @@ const Heading: React.FC<Heading1to5Props | Heading6Props | HeadingLabelProps> =
className = '', className = '',
...props ...props
}) => { }) => {
if (!level) {
level = 1;
}
const newElement = `${useLabelTag ? 'label' : `h${level}`}`; const newElement = `${useLabelTag ? 'label' : `h${level}`}`;
styles += (level === 6 || useLabelTag) ? (` block ${grey ? Heading6StylesGrey : Heading6Styles}`) : ' '; styles += (level === 6 || useLabelTag) ? (` block ${grey ? Heading6StylesGrey : Heading6Styles}`) : ' ';
const Element = React.createElement(newElement, {className: styles + ' ' + className, key: 'heading-elem', ...props}, children); if (!useLabelTag) {
switch (level) {
case 1:
styles += ' md:text-5xl';
break;
case 2:
styles += ' md:text-3xl';
break;
case 3:
styles += ' md:text-2xl';
break;
case 4:
styles += ' md:text-xl';
break;
case 5:
styles += ' md:text-lg';
break;
default:
break;
}
}
className = clsx(
styles,
className
);
const Element = React.createElement(newElement, {className: className, key: 'heading-elem', ...props}, children);
if (separator) { if (separator) {
let gap = (!level || level === 1) ? 2 : 1; let gap = (!level || level === 1) ? 2 : 1;

View file

@ -42,7 +42,7 @@ const ListItem: React.FC<ListItemProps> = ({
}; };
const listItemClasses = clsx( const listItemClasses = clsx(
'group flex items-center justify-between', 'group/list-item flex items-center justify-between',
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50', bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200' : 'border-y border-transparent hover:border-grey-200 first-of-type:hover:border-t-transparent', separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200' : 'border-y border-transparent hover:border-grey-200 first-of-type:hover:border-t-transparent',
className className
@ -60,7 +60,7 @@ const ListItem: React.FC<ListItemProps> = ({
</div> </div>
} }
{action && {action &&
<div className={`py-3 pl-6 ${paddingRight && 'pr-6'} ${hideActions ? 'invisible group-hover:visible' : ''}`}> <div className={`visible py-3 md:pl-6 ${paddingRight && 'md:pr-6'} ${hideActions ? 'group-hover/list-item:visible md:invisible' : ''}`}>
{action} {action}
</div> </div>
} }

View file

@ -23,7 +23,10 @@ type Story = StoryObj<typeof TabView>;
const tabs = [ const tabs = [
{id: 'tab-1', title: 'Tab one', contents: <div className='py-5'>Contents one</div>}, {id: 'tab-1', title: 'Tab one', contents: <div className='py-5'>Contents one</div>},
{id: 'tab-2', title: 'Tab two', contents: <div className='py-5'>Contents two</div>}, {id: 'tab-2', title: 'Tab two', contents: <div className='py-5'>Contents two</div>},
{id: 'tab-3', title: 'Tab three', contents: <div className='py-5'>Contents three</div>} {id: 'tab-3', title: 'Tab three', contents: <div className='py-5'>Contents three</div>},
{id: 'tab-4', title: 'Tab four', contents: <div className='py-5'>Contents one</div>},
{id: 'tab-5', title: 'Tab five', contents: <div className='py-5'>Contents two</div>},
{id: 'tab-6', title: 'Backstreet boys', contents: <div className='py-5'>Contents three</div>}
]; ];
export const Default: Story = { export const Default: Story = {

View file

@ -40,7 +40,7 @@ function TabView<ID extends string = string>({
}; };
const containerClasses = clsx( const containerClasses = clsx(
'flex', 'flex w-full overflow-x-scroll',
width === 'narrow' && 'gap-3', width === 'narrow' && 'gap-3',
width === 'normal' && 'gap-5', width === 'normal' && 'gap-5',
width === 'wide' && 'gap-7', width === 'wide' && 'gap-7',
@ -55,7 +55,7 @@ function TabView<ID extends string = string>({
key={tab.id} key={tab.id}
aria-selected={selectedTab === tab.id} aria-selected={selectedTab === tab.id}
className={clsx( className={clsx(
'-m-b-px cursor-pointer appearance-none py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)]', '-m-b-px cursor-pointer appearance-none whitespace-nowrap py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)]',
border && 'border-b-[3px]', border && 'border-b-[3px]',
selectedTab === tab.id && border ? 'border-black' : 'border-transparent hover:border-grey-500', selectedTab === tab.id && border ? 'border-black' : 'border-transparent hover:border-grey-500',
selectedTab === tab.id && 'font-bold' selectedTab === tab.id && 'font-bold'

View file

@ -25,7 +25,7 @@ const TableRow: React.FC<TableRowProps> = ({id, action, hideActions, className,
separator = (separator === undefined) ? true : separator; separator = (separator === undefined) ? true : separator;
const tableRowClasses = clsx( const tableRowClasses = clsx(
'group', 'group/table-row',
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50', bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50',
onClick && 'cursor-pointer', onClick && 'cursor-pointer',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200' : 'border-y border-transparent first-of-type:hover:border-t-transparent', separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200' : 'border-y border-transparent first-of-type:hover:border-t-transparent',
@ -36,7 +36,7 @@ const TableRow: React.FC<TableRowProps> = ({id, action, hideActions, className,
<tr className={tableRowClasses} data-testid={testId} id={id} onClick={handleClick}> <tr className={tableRowClasses} data-testid={testId} id={id} onClick={handleClick}>
{children} {children}
{action && {action &&
<td className={`px-6 py-3 text-center ${hideActions ? 'invisible group-hover:visible' : ''}`}> <td className={`visible block px-6 py-3 text-center ${hideActions ? 'group-hover/table-row:visible md:invisible' : ''}`}>
{action} {action}
</td> </td>
} }

View file

@ -58,7 +58,7 @@ const Form: React.FC<FormProps> = ({
if (grouped) { if (grouped) {
classes = clsx( classes = clsx(
classes, classes,
'rounded-sm border border-grey-200 p-7' 'rounded-sm border border-grey-200 p-4 md:p-7'
); );
} }

View file

@ -71,7 +71,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
if (!deleteButtonUnstyled) { if (!deleteButtonUnstyled) {
deleteButtonClassName = clsx( deleteButtonClassName = clsx(
'invisible absolute right-4 top-4 flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-[rgba(0,0,0,0.75)] text-white hover:bg-black group-hover:!visible', 'absolute right-4 top-4 flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-[rgba(0,0,0,0.75)] text-white hover:bg-black group-hover:!visible md:invisible',
deleteButtonClassName deleteButtonClassName
); );
} }

View file

@ -29,6 +29,7 @@ export interface ModalProps {
onOk?: () => void; onOk?: () => void;
onCancel?: () => void; onCancel?: () => void;
topRightContent?: 'close' | React.ReactNode; topRightContent?: 'close' | React.ReactNode;
hideXOnMobile?: boolean;
afterClose?: () => void; afterClose?: () => void;
children?: React.ReactNode; children?: React.ReactNode;
backDrop?: boolean; backDrop?: boolean;
@ -54,6 +55,7 @@ const Modal: React.FC<ModalProps> = ({
okColor = 'black', okColor = 'black',
onCancel, onCancel,
topRightContent, topRightContent,
hideXOnMobile = false,
afterClose, afterClose,
children, children,
backDrop = true, backDrop = true,
@ -144,31 +146,31 @@ const Modal: React.FC<ModalProps> = ({
switch (size) { switch (size) {
case 'sm': case 'sm':
modalClasses += ' max-w-[480px] '; modalClasses += ' max-w-[480px] ';
backdropClasses += ' p-[8vmin]'; backdropClasses += ' p-4 md:p-[8vmin]';
paddingClasses = 'p-8'; paddingClasses = 'p-8';
break; break;
case 'md': case 'md':
modalClasses += ' max-w-[720px] '; modalClasses += ' max-w-[720px] ';
backdropClasses += ' p-[8vmin]'; backdropClasses += ' p-4 md:p-[8vmin]';
paddingClasses = 'p-8'; paddingClasses = 'p-8';
break; break;
case 'lg': case 'lg':
modalClasses += ' max-w-[1020px] '; modalClasses += ' max-w-[1020px] ';
backdropClasses += ' p-[4vmin]'; backdropClasses += ' p-4 md:p-[4vmin]';
paddingClasses = 'p-8'; paddingClasses = 'p-8';
break; break;
case 'xl': case 'xl':
modalClasses += ' max-w-[1240px] '; modalClasses += ' max-w-[1240px] ';
backdropClasses += ' p-[3vmin]'; backdropClasses += ' p-4 md:p-[3vmin]';
paddingClasses = 'p-10'; paddingClasses = 'p-10';
break; break;
case 'full': case 'full':
modalClasses += ' h-full '; modalClasses += ' h-full ';
backdropClasses += ' p-[3vmin]'; backdropClasses += ' p-4 md:p-[3vmin]';
paddingClasses = 'p-10'; paddingClasses = 'p-10';
break; break;
@ -178,7 +180,7 @@ const Modal: React.FC<ModalProps> = ({
break; break;
default: default:
backdropClasses += ' p-[8vmin]'; backdropClasses += ' p-4 md:p-[8vmin]';
paddingClasses = 'p-8'; paddingClasses = 'p-8';
break; break;
} }
@ -187,6 +189,9 @@ const Modal: React.FC<ModalProps> = ({
paddingClasses = 'p-0'; paddingClasses = 'p-0';
} }
// Set bottom padding for backdrop when the menu is on
backdropClasses += ' max-[800px]:!pb-20';
let footerClasses = clsx( let footerClasses = clsx(
`${paddingClasses} ${stickyFooter ? 'py-6' : 'pt-0'}`, `${paddingClasses} ${stickyFooter ? 'py-6' : 'pt-0'}`,
'flex w-full items-center justify-between' 'flex w-full items-center justify-between'
@ -204,7 +209,7 @@ const Modal: React.FC<ModalProps> = ({
}; };
const modalStyles = (typeof size === 'number') ? { const modalStyles = (typeof size === 'number') ? {
width: size + 'px' maxWidth: size + 'px'
} : {}; } : {};
let footerContent; let footerContent;
@ -247,10 +252,10 @@ const Modal: React.FC<ModalProps> = ({
<section className={modalClasses} data-testid={testId} style={modalStyles}> <section className={modalClasses} data-testid={testId} style={modalStyles}>
<div className={contentClasses}> <div className={contentClasses}>
<div className='h-full'> <div className='h-full'>
{topRightContent === 'close' ? {!topRightContent || topRightContent === 'close' ?
(<> (<>
{title && <Heading level={3}>{title}</Heading>} {title && <Heading level={3}>{title}</Heading>}
<div className='absolute right-6 top-6'> <div className={`${topRightContent !== 'close' && 'md:!invisible md:!hidden'} ${hideXOnMobile && 'hidden'} absolute right-6 top-6`}>
<Button className='-m-2 cursor-pointer p-2 opacity-50 hover:opacity-100' icon='close' size='sm' unstyled onClick={removeModal} /> <Button className='-m-2 cursor-pointer p-2 opacity-50 hover:opacity-100' icon='close' size='sm' unstyled onClick={removeModal} />
</div> </div>
</>) </>)

View file

@ -209,16 +209,17 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
size={size} size={size}
testId={testId} testId={testId}
title='' title=''
hideXOnMobile
> >
<div className='flex h-full grow'> <div className='flex h-full grow'>
<div className={`flex grow flex-col ${previewBgColor === 'grey' ? 'bg-grey-50' : 'bg-white'}`}> <div className={`hidden grow flex-col md:!visible md:!flex ${previewBgColor === 'grey' ? 'bg-grey-50' : 'bg-white'}`}>
{preview} {preview}
</div> </div>
{sidebar && {sidebar &&
<div className='relative flex h-full basis-[400px] flex-col border-l border-grey-100'> <div className='relative flex h-full w-full flex-col border-l border-grey-100 md:w-auto md:basis-[400px]'>
{sidebarHeader ? sidebarHeader : ( {sidebarHeader ? sidebarHeader : (
<div className='flex max-h-[74px] items-start justify-between gap-3 px-7 py-5'> <div className='flex max-h-[74px] items-center justify-between gap-3 px-7 py-5'>
<Heading className='mt-1' level={titleHeadingLevel}>{title}</Heading> <Heading level={titleHeadingLevel}>{title}</Heading>
{sidebarButtons ? sidebarButtons : <ButtonGroup buttons={buttons} /> } {sidebarButtons ? sidebarButtons : <ButtonGroup buttons={buttons} /> }
</div> </div>
)} )}

View file

@ -167,6 +167,7 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
border && 'border p-5 md:p-7', border && 'border p-5 md:p-7',
!checkVisible(keywords) ? 'hidden' : 'flex', !checkVisible(keywords) ? 'hidden' : 'flex',
highlight && 'before:pointer-events-none before:absolute before:inset-[1px] before:z-20 before:animate-setting-highlight-fade-out before:rounded before:shadow-[0_0_0_3px_rgba(48,207,67,0.45)]', highlight && 'before:pointer-events-none before:absolute before:inset-[1px] before:z-20 before:animate-setting-highlight-fade-out before:rounded before:shadow-[0_0_0_3px_rgba(48,207,67,0.45)]',
!isEditing && 'is-not-editing group',
styles styles
); );

View file

@ -17,7 +17,7 @@ interface ISettingGroupContent {
const SettingGroupContent: React.FC<ISettingGroupContent> = ({columns, values, children, className}) => { const SettingGroupContent: React.FC<ISettingGroupContent> = ({columns, values, children, className}) => {
let styles = 'flex flex-col gap-x-6 gap-y-7'; let styles = 'flex flex-col gap-x-6 gap-y-7';
if (columns === 2) { if (columns === 2) {
styles = 'grid grid-cols-2 gap-x-8 gap-y-6'; styles = 'grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6';
} }
styles += ` ${className}`; styles += ` ${className}`;

View file

@ -13,10 +13,12 @@ const SettingGroupHeader: React.FC<Props> = ({title, description, children}) =>
{(title || description) && {(title || description) &&
<div> <div>
<Heading level={5}>{title}</Heading> <Heading level={5}>{title}</Heading>
{description && <p className="mt-0.5 max-w-lg text-sm">{description}</p>} {description && <p className="mt-0.5 hidden max-w-lg text-sm group-[.is-not-editing]:!visible group-[.is-not-editing]:!block md:!visible md:!block">{description}</p>}
</div> </div>
} }
{children} <div className='-mt-0.5'>
{children}
</div>
</div> </div>
); );
}; };

View file

@ -25,58 +25,59 @@ const Sidebar: React.FC = () => {
const hasRecommendations = useFeatureFlag('recommendations'); const hasRecommendations = useFeatureFlag('recommendations');
return ( return (
<div className="hidden md:!visible md:!block md:h-[calc(100vh-5vmin-84px)] md:w-[240px] md:overflow-y-scroll md:pt-[32px]"> <div className='tablet:h-[calc(100vh-5vmin-84px)] tablet:w-[240px] tablet:overflow-y-scroll'>
<div className='relative mb-10'> <div className='relative mb-10 md:pt-4 tablet:pt-[32px]'>
<Icon className='absolute top-2' 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 px-3 py-1.5 pl-[24px] text-sm' placeholder="Search" title="Search" value={filter} hideTitle unstyled onChange={e => setFilter(e.target.value)} /> <TextField autoComplete="off" className='border-b border-grey-500 px-3 py-1.5 pl-[24px] text-sm' placeholder="Search" title="Search" value={filter} hideTitle unstyled onChange={e => setFilter(e.target.value)} />
</div> </div>
<div className="hidden tablet:!visible tablet:!block">
<SettingNavSection title="General">
<SettingNavItem navid='title-and-description' title="Title and description" onClick={handleSectionClick} />
<SettingNavItem navid='timezone' title="Timezone" onClick={handleSectionClick} />
<SettingNavItem navid='publication-language' title="Publication language" onClick={handleSectionClick} />
<SettingNavItem navid='metadata' title="Meta data" onClick={handleSectionClick} />
<SettingNavItem navid='twitter' title="Twitter card" onClick={handleSectionClick} />
<SettingNavItem navid='facebook' title="Facebook card" onClick={handleSectionClick} />
<SettingNavItem navid='social-accounts' title="Social accounts" onClick={handleSectionClick} />
<SettingNavItem navid='locksite' title="Make this site private" onClick={handleSectionClick} />
<SettingNavItem navid='users' title="Users and permissions" onClick={handleSectionClick} />
</SettingNavSection>
<SettingNavSection title="General"> <SettingNavSection title="Site">
<SettingNavItem navid='title-and-description' title="Title and description" onClick={handleSectionClick} /> {/* <SettingNavItem navid='theme' title="Theme" onClick={handleSectionClick} /> */}
<SettingNavItem navid='timezone' title="Timezone" onClick={handleSectionClick} /> <SettingNavItem navid='design' title="Branding and design" onClick={handleSectionClick} />
<SettingNavItem navid='publication-language' title="Publication language" onClick={handleSectionClick} /> <SettingNavItem navid='navigation' title="Navigation" onClick={handleSectionClick} />
<SettingNavItem navid='metadata' title="Meta data" onClick={handleSectionClick} /> <SettingNavItem navid='announcement-bar' title="Announcement bar" onClick={handleSectionClick} />
<SettingNavItem navid='twitter' title="Twitter card" onClick={handleSectionClick} /> </SettingNavSection>
<SettingNavItem navid='facebook' title="Facebook card" onClick={handleSectionClick} />
<SettingNavItem navid='social-accounts' title="Social accounts" onClick={handleSectionClick} />
<SettingNavItem navid='locksite' title="Make this site private" onClick={handleSectionClick} />
<SettingNavItem navid='users' title="Users and permissions" onClick={handleSectionClick} />
</SettingNavSection>
<SettingNavSection title="Site"> <SettingNavSection title="Membership">
{/* <SettingNavItem navid='theme' title="Theme" onClick={handleSectionClick} /> */} <SettingNavItem navid='portal' title="Portal" onClick={handleSectionClick} />
<SettingNavItem navid='design' title="Branding and design" onClick={handleSectionClick} /> <SettingNavItem navid='access' title="Access" onClick={handleSectionClick} />
<SettingNavItem navid='navigation' title="Navigation" onClick={handleSectionClick} /> <SettingNavItem navid='tiers' title="Tiers" onClick={handleSectionClick} />
<SettingNavItem navid='announcement-bar' title="Announcement bar" onClick={handleSectionClick} /> {hasTipsAndDonations && <SettingNavItem navid='tips-or-donations' title="Tips or donations" onClick={handleSectionClick} />}
</SettingNavSection> <SettingNavItem navid='embed-signup-form' title="Embeddable signup form" onClick={handleSectionClick} />
{hasRecommendations && <SettingNavItem navid='recommendations' title="Recommendations" onClick={handleSectionClick} />}
<SettingNavItem navid='analytics' title="Analytics" onClick={handleSectionClick} />
</SettingNavSection>
<SettingNavSection title="Membership"> <SettingNavSection title="Email newsletters">
<SettingNavItem navid='portal' title="Portal" onClick={handleSectionClick} /> <SettingNavItem navid='enable-newsletters' title="Newsletter sending" onClick={handleSectionClick} />
<SettingNavItem navid='access' title="Access" onClick={handleSectionClick} /> {newslettersEnabled !== 'disabled' && (
<SettingNavItem navid='tiers' title="Tiers" onClick={handleSectionClick} /> <>
{hasTipsAndDonations && <SettingNavItem navid='tips-or-donations' title="Tips or donations" onClick={handleSectionClick} />} <SettingNavItem navid='newsletters' title="Newsletters" onClick={handleSectionClick} />
<SettingNavItem navid='embed-signup-form' title="Embeddable signup form" onClick={handleSectionClick} /> <SettingNavItem navid='default-recipients' title="Default recipients" onClick={handleSectionClick} />
{hasRecommendations && <SettingNavItem navid='recommendations' title="Recommendations" onClick={handleSectionClick} />} <SettingNavItem navid='mailgun' title="Mailgun settings" onClick={handleSectionClick} />
<SettingNavItem navid='analytics' title="Analytics" onClick={handleSectionClick} /> </>
</SettingNavSection> )}
</SettingNavSection>
<SettingNavSection title="Email newsletters"> <SettingNavSection title="Advanced">
<SettingNavItem navid='enable-newsletters' title="Newsletter sending" onClick={handleSectionClick} /> <SettingNavItem navid='integrations' title="Integrations" onClick={handleSectionClick} />
{newslettersEnabled !== 'disabled' && ( <SettingNavItem navid='code-injection' title="Code injection" onClick={handleSectionClick} />
<> <SettingNavItem navid='labs' title="Labs" onClick={handleSectionClick} />
<SettingNavItem navid='newsletters' title="Newsletters" onClick={handleSectionClick} /> <SettingNavItem navid='history' title="History" onClick={handleSectionClick} />
<SettingNavItem navid='default-recipients' title="Default recipients" onClick={handleSectionClick} /> </SettingNavSection>
<SettingNavItem navid='mailgun' title="Mailgun settings" onClick={handleSectionClick} /> </div>
</>
)}
</SettingNavSection>
<SettingNavSection title="Advanced">
<SettingNavItem navid='integrations' title="Integrations" onClick={handleSectionClick} />
<SettingNavItem navid='code-injection' title="Code injection" onClick={handleSectionClick} />
<SettingNavItem navid='labs' title="Labs" onClick={handleSectionClick} />
<SettingNavItem navid='history' title="History" onClick={handleSectionClick} />
</SettingNavSection>
</div> </div>
); );
}; };

View file

@ -31,7 +31,7 @@ const HistoryIcon: React.FC<{action: Action}> = ({action}) => {
const HistoryAvatar: React.FC<{action: Action}> = ({action}) => { const HistoryAvatar: React.FC<{action: Action}> = ({action}) => {
return ( return (
<div className='relative'> <div className='relative shrink-0'>
<Avatar <Avatar
bgColor={generateAvatarColor(action.actor?.name || action.actor?.slug || '')} bgColor={generateAvatarColor(action.actor?.name || action.actor?.slug || '')}
image={action.actor?.image} image={action.actor?.image}
@ -68,7 +68,7 @@ const HistoryFilter: React.FC<{
toggleResourceType: (resource: string, included: boolean) => void; toggleResourceType: (resource: string, included: boolean) => void;
}> = ({excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => { }> = ({excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => {
return ( return (
<Popover trigger={<Button label='Filter' link />}> <Popover position='right' trigger={<Button label='Filter' link />}>
<div className='flex w-[220px] flex-col gap-8 p-5'> <div className='flex w-[220px] flex-col gap-8 p-5'>
<ToggleGroup> <ToggleGroup>
<HistoryFilterToggle excludedItems={excludedEvents} item='added' label='Added' toggleItem={toggleEventType} /> <HistoryFilterToggle excludedItems={excludedEvents} item='added' label='Added' toggleItem={toggleEventType} />

View file

@ -192,7 +192,7 @@ const Integrations: React.FC<{ keywords: string[] }> = ({keywords}) => {
] as const; ] as const;
const buttons = ( const buttons = (
<Button color='green' label='Add custom integration' link={true} onClick={() => { <Button className='hidden md:!visible md:!block' color='green' label='Add custom integration' link={true} onClick={() => {
updateRoute('integrations/add'); updateRoute('integrations/add');
setSelectedTab('custom'); setSelectedTab('custom');
}} /> }} />
@ -207,6 +207,12 @@ const Integrations: React.FC<{ keywords: string[] }> = ({keywords}) => {
testId='integrations' testId='integrations'
title="Integrations" title="Integrations"
> >
<div className='flex justify-center rounded border border-green px-4 py-2 md:hidden'>
<Button color='green' label='Add custom integration' link onClick={() => {
updateRoute('integrations/add');
setSelectedTab('custom');
}} />
</div>
<TabView<'built-in' | 'custom'> selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} /> <TabView<'built-in' | 'custom'> selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
</SettingGroup> </SettingGroup>
); );

View file

@ -18,11 +18,11 @@ const APIKeyField: React.FC<APIKeyFieldProps> = ({label, text = '', hint, onRege
}; };
return <> return <>
<div className='p-0 py-1 pr-4 text-sm text-grey-600'>{label}</div> <div className='p-0 pr-4 text-sm text-grey-600 md:py-1'>{label}</div>
<div className='group relative overflow-hidden rounded p-1 text-sm hover:bg-grey-50'> <div className='group/api-keys relative mb-3 overflow-hidden rounded py-1 text-sm hover:bg-grey-50 md:mb-0 md:p-1'>
{text} {text}
{hint} {hint}
<div className='invisible absolute right-0 top-[50%] flex translate-y-[-50%] gap-1 bg-white pl-1 text-sm group-hover:visible'> <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 md:invisible'>
{onRegenerate && <Button color='outline' label='Regenerate' size='sm' onClick={onRegenerate} />} {onRegenerate && <Button color='outline' label='Regenerate' size='sm' onClick={onRegenerate} />}
<Button color='outline' label={copied ? 'Copied' : 'Copy'} size='sm' onClick={copyText} /> <Button color='outline' label={copied ? 'Copied' : 'Copy'} size='sm' onClick={copyText} />
</div> </div>
@ -32,7 +32,7 @@ const APIKeyField: React.FC<APIKeyFieldProps> = ({label, text = '', hint, onRege
const APIKeys: React.FC<{keys: APIKeyFieldProps[]}> = ({keys}) => { const APIKeys: React.FC<{keys: APIKeyFieldProps[]}> = ({keys}) => {
return ( return (
<div className='grid grid-cols-[max-content_1fr]'> <div className='grid grid-cols-1 md:grid-cols-[max-content_1fr]'>
{keys.map(key => <APIKeyField key={key.label} {...key} />)} {keys.map(key => <APIKeyField key={key.label} {...key} />)}
</div> </div>
); );

View file

@ -14,7 +14,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
extra extra
}) => { }) => {
return ( return (
<div className='flex w-full gap-4'> <div className='flex w-full flex-col gap-4 md:flex-row'>
<div className='h-14 w-14'>{icon}</div> <div className='h-14 w-14'>{icon}</div>
<div className='flex min-w-0 flex-1 flex-col'> <div className='flex min-w-0 flex-1 flex-col'>
<h3>{title}</h3> <h3>{title}</h3>

View file

@ -99,17 +99,18 @@ const PinturaModal = NiceModal.create(() => {
title='Pintura' title='Pintura'
/> />
<div className='mt-7'> <div className='mt-7'>
<div className='mb-7 flex items-stretch justify-between gap-4 rounded-sm bg-grey-75 p-7'> <div className='mb-7 flex flex-col items-stretch justify-between gap-4 rounded-sm bg-grey-75 p-4 md:flex-row md:p-7'>
<div className='basis-1/2'> <div className='md:basis-1/2'>
<p className='mb-4 font-bold'>Add advanced image editing to Ghost, with Pintura</p> <p className='mb-4 font-bold'>Add advanced image editing to Ghost, with Pintura</p>
<p className='mb-4 text-sm'>Pintura is a powerful JavaScript image editor that allows you to crop, rotate, annotate and modify images directly inside Ghost.</p> <p className='mb-4 text-sm'>Pintura is a powerful JavaScript image editor that allows you to crop, rotate, annotate and modify images directly inside Ghost.</p>
<p className='text-sm'>Try a demo, purchase a license, and download the required CSS/JS files from pqina.nl/pintura/ to activate this feature.</p> <p className='text-sm'>Try a demo, purchase a license, and download the required CSS/JS files from pqina.nl/pintura/ to activate this feature.</p>
</div> </div>
<div className='flex grow basis-1/2 flex-col items-end justify-between'> <div className='flex grow flex-col items-end justify-between gap-2 md:basis-1/2 md:gap-0'>
<img alt='Pintura screenshot' src={pinturaScreenshot} /> <img alt='Pintura screenshot' src={pinturaScreenshot} />
<a className='-mb-1 text-sm font-bold text-green' href="https://pqina.nl/pintura/?ref=ghost.org" rel="noopener noreferrer" target="_blank">Find out more &rarr;</a> <a className='-mb-1 text-sm font-bold text-green' href="https://pqina.nl/pintura/?ref=ghost.org" rel="noopener noreferrer" target="_blank">Find out more &rarr;</a>
</div> </div>
</div> </div>
<Form marginBottom={false} title='Pintura configuration' grouped> <Form marginBottom={false} title='Pintura configuration' grouped>
<Toggle <Toggle
checked={enabled} checked={enabled}
@ -122,7 +123,7 @@ const PinturaModal = NiceModal.create(() => {
/> />
{enabled && ( {enabled && (
<> <>
<div className='flex items-center justify-between'> <div className='flex flex-col justify-between gap-1 md:flex-row md:items-center'>
<div> <div>
<div>Upload Pintura script</div> <div>Upload Pintura script</div>
<div className='text-xs text-grey-600'>Upload the <code>pintura-umd.js</code> file from the Pintura package</div> <div className='text-xs text-grey-600'>Upload the <code>pintura-umd.js</code> file from the Pintura package</div>
@ -134,7 +135,7 @@ const PinturaModal = NiceModal.create(() => {
triggerUpload('js'); triggerUpload('js');
}} /> }} />
</div> </div>
<div className='flex items-center justify-between'> <div className='flex flex-col justify-between gap-1 md:flex-row md:items-center'>
<div> <div>
<div>Upload Pintura styles</div> <div>Upload Pintura styles</div>
<div className='text-xs text-grey-600'>Upload the <code>pintura.css</code> file from the Pintura package</div> <div className='text-xs text-grey-600'>Upload the <code>pintura.css</code> file from the Pintura package</div>

View file

@ -90,7 +90,7 @@ const SlackModal = NiceModal.create(() => {
onChange={e => updateSetting('slack_url', e.target.value)} onChange={e => updateSetting('slack_url', e.target.value)}
onKeyDown={() => clearError('slackUrl')} onKeyDown={() => clearError('slackUrl')}
/> />
<div className='flex w-full items-center gap-2'> <div className='flex w-full flex-col gap-2 md:flex-row md:items-center'>
<TextField <TextField
containerClassName='flex-grow' containerClassName='flex-grow'
hint='The username to display messages from' hint='The username to display messages from'

View file

@ -97,28 +97,31 @@ const ZapierModal = NiceModal.create(() => {
{zapierTemplates.map(template => ( {zapierTemplates.map(template => (
<ListItem <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 />} action={<Button className='whitespace-nowrap text-sm font-semibold text-[#FF4A00]' href={template.url} label='Use this Zap' tag='a' target='_blank' link unstyled />}
avatar={<>
<img className='h-8 w-8 object-contain' role='presentation' src={`${adminRoot}${template.ghostImage}`} />
<ArrowRightIcon className='h-3 w-3' />
<img className='h-8 w-8 object-contain' role='presentation' src={`${adminRoot}${template.appImage}`} />
</>}
bgOnHover={false} bgOnHover={false}
className='flex items-center gap-3 py-2' className='flex items-center gap-3 py-2'
title={<span className='text-sm'>{template.title}</span>} 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'>
<img className='h-8 w-8 object-contain' role='presentation' src={`${adminRoot}${template.ghostImage}`} />
<ArrowRightIcon className='h-3 w-3' />
<img className='h-8 w-8 object-contain' role='presentation' src={`${adminRoot}${template.appImage}`} />
</div>
<span className='text-sm'>{template.title}</span>
</div>
}
hideActions hideActions
/> />
))} ))}
</List> </List>
<div className='mt-6 flex'> <div className='mt-6'>
<Button <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' href='https://zapier.com/apps/ghost/integrations?utm_medium=partner_api&utm_source=widget&utm_campaign=Widget'
label={<>View more Ghost integrations powered by <Logo className='relative top-[-1px] ml-1 h-6' /></>}
rel='noopener noreferrer' rel='noopener noreferrer'
tag='a' target='_blank'>
target='_blank' View more Ghost integrations powered by <span><Logo className='relative top-[-2px] inline-block h-6' /></span>
link </a>
/>
</div> </div>
</Modal> </Modal>
); );

View file

@ -76,13 +76,13 @@ const NewsletterItem: React.FC<{newsletter: Newsletter, onlyOne: boolean}> = ({n
<span className='whitespace-nowrap text-xs text-grey-700'>{newsletter.description || 'No description'}</span> <span className='whitespace-nowrap text-xs text-grey-700'>{newsletter.description || 'No description'}</span>
</div> </div>
</TableCell> </TableCell>
<TableCell onClick={showDetails}> <TableCell className='hidden md:!visible md:!table-cell' onClick={showDetails}>
<div className={`flex grow flex-col`}> <div className={`flex grow flex-col`}>
<span>{newsletter.count?.active_members}</span> <span>{newsletter.count?.active_members}</span>
<span className='whitespace-nowrap text-xs text-grey-700'>Subscribers</span> <span className='whitespace-nowrap text-xs text-grey-700'>Subscribers</span>
</div> </div>
</TableCell> </TableCell>
<TableCell onClick={showDetails}> <TableCell className='hidden md:!visible md:!table-cell' onClick={showDetails}>
<div className={`flex grow flex-col`}> <div className={`flex grow flex-col`}>
<span>{newsletter.count?.posts}</span> <span>{newsletter.count?.posts}</span>
<span className='whitespace-nowrap text-xs text-grey-700'>Posts sent</span> <span className='whitespace-nowrap text-xs text-grey-700'>Posts sent</span>

View file

@ -48,7 +48,7 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
); );
const inputFields = ( const inputFields = (
<div className="mx-[52px]"> <div className="md:mx-[52px]">
<div className="mb-4 flex items-center gap-2"> <div className="mb-4 flex items-center gap-2">
<div> <div>
<FacebookLogo className='h-10 w-10' /> <FacebookLogo className='h-10 w-10' />

View file

@ -51,7 +51,7 @@ const LockSite: React.FC<{ keywords: string[] }> = ({keywords}) => {
); );
const hint = ( const hint = (
<>A private RSS feed is available at <Link href="http://localhost:2368/51aa059ba6eb50c24c14047d4255ac/rss">http://localhost:2368/51aa059ba6eb50c24c14047d4255ac/rss</Link></> <>A private RSS feed is available at <Link className='break-all' href="http://localhost:2368/51aa059ba6eb50c24c14047d4255ac/rss">http://localhost:2368/51aa059ba6eb50c24c14047d4255ac/rss</Link></>
); );
const inputs = ( const inputs = (

View file

@ -52,11 +52,11 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
); );
const inputFields = ( const inputFields = (
<div className="flex gap-3"> <div className="flex flex-col gap-3 md:flex-row">
<div className="pt-1"> <div className="pt-1">
<TwitterLogo className='-mb-1 h-10 w-10' /> <TwitterLogo className='-mb-1 h-10 w-10' />
</div> </div>
<div className="mr-[52px] w-full"> <div className="w-full md:mr-[52px]">
<div className="mb-2"> <div className="mb-2">
<span className="mr-1 font-semibold text-grey-900">{siteTitle}</span> <span className="mr-1 font-semibold text-grey-900">{siteTitle}</span>
<span className="text-grey-700">&#183; 2h</span> <span className="text-grey-700">&#183; 2h</span>

View file

@ -629,7 +629,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
okLabel = 'Saved'; okLabel = 'Saved';
} }
const fileUploadButtonClasses = 'absolute right-[104px] bottom-12 bg-[rgba(0,0,0,0.75)] rounded text-sm text-white flex items-center justify-center px-3 h-8 opacity-80 hover:opacity-100 transition cursor-pointer font-medium z-10'; const fileUploadButtonClasses = 'absolute left-12 md:left-auto md:right-[104px] bottom-12 bg-[rgba(0,0,0,0.75)] rounded text-sm text-white flex items-center justify-center px-3 h-8 opacity-80 hover:opacity-100 transition cursor-pointer font-medium z-10';
const suspendedText = userData.status === 'inactive' ? ' (Suspended)' : ''; const suspendedText = userData.status === 'inactive' ? ' (Suspended)' : '';
@ -706,11 +706,11 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
}} }}
>Upload cover image</ImageUpload> >Upload cover image</ImageUpload>
<div className="absolute bottom-12 right-12 z-10"> <div className="absolute bottom-12 right-12 z-10">
<Menu items={menuItems} position='left' trigger={<UserMenuTrigger />}></Menu> <Menu items={menuItems} position='right' trigger={<UserMenuTrigger />}></Menu>
</div> </div>
<div className='relative flex items-center gap-4 px-12 pb-7 pt-60'> <div className='relative flex flex-col items-start gap-4 px-12 pb-60 pt-10 md:flex-row md:items-center md:pb-7 md:pt-60'>
<ImageUpload <ImageUpload
deleteButtonClassName='invisible absolute -right-2 -top-2 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.75)] text-white hover:bg-black group-hover:!visible' deleteButtonClassName='md:invisible absolute -right-2 -top-2 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.75)] text-white hover:bg-black group-hover:!visible'
deleteButtonContent={<Icon colorClass='text-white' name='trash' size='sm' />} deleteButtonContent={<Icon colorClass='text-white' name='trash' size='sm' />}
fileUploadClassName='rounded-full bg-black flex items-center justify-center opacity-80 transition hover:opacity-100 -ml-2 cursor-pointer h-[80px] w-[80px]' fileUploadClassName='rounded-full bg-black flex items-center justify-center opacity-80 transition hover:opacity-100 -ml-2 cursor-pointer h-[80px] w-[80px]'
id='avatar' id='avatar'
@ -734,7 +734,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
</div> </div>
</div> </div>
</div> </div>
<div className='mt-10 grid grid-cols-2 gap-x-12 gap-y-20'> <div className='mt-10 grid grid-cols-1 gap-x-12 gap-y-20 md:grid-cols-2'>
<Basic errors={errors} setUserData={setUserData} user={userData} validators={validators} /> <Basic errors={errors} setUserData={setUserData} user={userData} validators={validators} />
<Details errors={errors} setUserData={setUserData} user={userData} validators={validators} /> <Details errors={errors} setUserData={setUserData} user={userData} validators={validators} />
<EmailNotifications setUserData={setUserData} user={userData} /> <EmailNotifications setUserData={setUserData} user={userData} />

View file

@ -43,7 +43,7 @@ const Owner: React.FC<OwnerProps> = ({user}) => {
<div className='group flex gap-3 hover:cursor-pointer' data-testid='owner-user' onClick={showDetailModal}> <div className='group flex gap-3 hover:cursor-pointer' data-testid='owner-user' onClick={showDetailModal}>
<Avatar bgColor={generateAvatarColor((user.name ? user.name : user.email))} image={user.profile_image} label={getInitials(user.name)} labelColor='white' size='lg' /> <Avatar bgColor={generateAvatarColor((user.name ? user.name : user.email))} image={user.profile_image} label={getInitials(user.name)} labelColor='white' size='lg' />
<div className='flex flex-col'> <div className='flex flex-col'>
<span>{user.name} &mdash; <strong>Owner</strong> <button className='invisible ml-2 inline-block text-sm font-bold text-green group-hover:visible' type='button'>Edit</button></span> <span>{user.name} &mdash; <strong>Owner</strong> <button className='ml-2 inline-block text-sm font-bold text-green group-hover:visible md:invisible' type='button'>Edit</button></span>
<span className='text-xs text-grey-700'>{user.email}</span> <span className='text-xs text-grey-700'>{user.email}</span>
</div> </div>
</div> </div>

View file

@ -3,11 +3,25 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import StripeButton from '../../../admin-x-ds/settings/StripeButton'; import StripeButton from '../../../admin-x-ds/settings/StripeButton';
import TabView from '../../../admin-x-ds/global/TabView'; import TabView from '../../../admin-x-ds/global/TabView';
import TiersList from './tiers/TiersList'; import TiersList from './tiers/TiersList';
import clsx from 'clsx';
import useRouting from '../../../hooks/useRouting'; import useRouting from '../../../hooks/useRouting';
import {Tier, getActiveTiers, getArchivedTiers, useBrowseTiers} from '../../../api/tiers'; import {Tier, getActiveTiers, getArchivedTiers, useBrowseTiers} from '../../../api/tiers';
import {checkStripeEnabled} from '../../../api/settings'; import {checkStripeEnabled} from '../../../api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider'; import {useGlobalData} from '../../providers/GlobalDataProvider';
const StripeConnectedButton: React.FC<{className?: string; onClick: () => void;}> = ({className, onClick}) => {
className = clsx(
'group flex shrink-0 items-center justify-center whitespace-nowrap rounded border border-grey-300 px-3 py-1.5 text-sm font-semibold text-grey-900 transition-all hover:border-grey-500',
className
);
return (
<button className={className} type='button' onClick={onClick}>
<span className="inline-flex h-2 w-2 rounded-full bg-green transition-all group-hover:bg-[#625BF6]"></span>
<span className='ml-2'>Connected to Stripe</span>
</button>
);
};
const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => { const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
const [selectedTab, setSelectedTab] = useState('active-tiers'); const [selectedTab, setSelectedTab] = useState('active-tiers');
const {settings, config} = useGlobalData(); const {settings, config} = useGlobalData();
@ -54,18 +68,23 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
return ( return (
<SettingGroup <SettingGroup
customButtons={checkStripeEnabled(settings, config) ? customButtons={checkStripeEnabled(settings, config) ?
<button className='group flex items-center gap-2 rounded border border-grey-300 px-3 py-1.5 text-sm font-semibold text-grey-900 transition-all hover:border-grey-500' type='button' onClick={openConnectModal}> <StripeConnectedButton className='hidden tablet:!visible tablet:!block' onClick={openConnectModal} />
<span className="inline-flex h-2 w-2 rounded-full bg-green transition-all group-hover:bg-[#625BF6]"></span>
Connected to Stripe
</button>
: :
<StripeButton onClick={openConnectModal}/>} <StripeButton className='hidden tablet:!visible tablet:!block' onClick={openConnectModal}/>}
description='Set prices and paid member sign up settings' description='Set prices and paid member sign up settings'
keywords={keywords} keywords={keywords}
navid='tiers' navid='tiers'
testId='tiers' testId='tiers'
title='Tiers' title='Tiers'
> >
<div className='w-full tablet:hidden'>
{checkStripeEnabled(settings, config) ?
<StripeConnectedButton className='w-full' onClick={openConnectModal} />
:
<StripeButton onClick={openConnectModal}/>
}
</div>
{content} {content}
</SettingGroup> </SettingGroup>
); );

View file

@ -193,13 +193,13 @@ const Connected: React.FC<{onClose?: () => void}> = ({onClose}) => {
<img alt='Ghost Logo' className='absolute left-10 h-16 w-16' src={GhostLogo} /> <img alt='Ghost Logo' className='absolute left-10 h-16 w-16' src={GhostLogo} />
<img alt='Stripe Logo' className='absolute right-10 h-16 w-16 rounded-2xl shadow-[-1.5px_0_0_1.5px_#fff]' src={StripeLogo} /> <img alt='Stripe Logo' className='absolute right-10 h-16 w-16 rounded-2xl shadow-[-1.5px_0_0_1.5px_#fff]' src={StripeLogo} />
</div> </div>
<Heading level={3}>You are connected with Stripe!{stripeConnectLivemode ? null : ' (Test mode)'}</Heading> <Heading className='text-center' level={3}>You are connected with Stripe!{stripeConnectLivemode ? null : ' (Test mode)'}</Heading>
<div className='mt-1'>Connected to <strong>Dummy</strong></div> <div className='mt-1'>Connected to <strong>Dummy</strong></div>
</div> </div>
<div className='flex flex-col items-center'> <div className='flex flex-col items-center'>
<Heading level={6}>Read next</Heading> <Heading level={6}>Read next</Heading>
<a className='w-100 mt-5 flex items-stretch justify-between border border-grey-200 transition-all hover:border-grey-400' href="https://ghost.org/resources/managing-your-stripe-account/?ref=admin" rel="noopener noreferrer" target="_blank"> <a className='w-100 mt-5 flex flex-col items-stretch justify-between border border-grey-200 transition-all hover:border-grey-400 md:flex-row' href="https://ghost.org/resources/managing-your-stripe-account/?ref=admin" rel="noopener noreferrer" target="_blank">
<div className='p-4'> <div className='order-2 p-4 md:order-1'>
<div className='font-bold'>How to setup and manage your Stripe account</div> <div className='font-bold'>How to setup and manage your Stripe account</div>
<div className='mt-1 text-sm text-grey-800'>Learn how to configure your Stripe account to work with Ghost, from custom branding to payment receipt emails.</div> <div className='mt-1 text-sm text-grey-800'>Learn how to configure your Stripe account to work with Ghost, from custom branding to payment receipt emails.</div>
<div className='mt-3 flex items-center gap-1 text-sm text-grey-800'> <div className='mt-3 flex items-center gap-1 text-sm text-grey-800'>
@ -209,7 +209,7 @@ const Connected: React.FC<{onClose?: () => void}> = ({onClose}) => {
<span>by Kym Ellis</span> <span>by Kym Ellis</span>
</div> </div>
</div> </div>
<div className='flex w-[200px] shrink-0 items-center justify-center overflow-hidden'> <div className='order-1 hidden w-[200px] shrink-0 items-center justify-center overflow-hidden md:!visible md:order-2 md:!flex'>
<img alt="Bookmark Thumb" className='min-h-full min-w-full shrink-0' src={BookmarkThumb} /> <img alt="Bookmark Thumb" className='min-h-full min-w-full shrink-0' src={BookmarkThumb} />
</div> </div>
</a> </a>
@ -292,6 +292,7 @@ const StripeConnectModal: React.FC = () => {
size={stripeConnectAccountId ? 740 : 520} size={stripeConnectAccountId ? 740 : 520}
testId='stripe-modal' testId='stripe-modal'
title='' title=''
hideXOnMobile
> >
{contents} {contents}
</Modal>; </Modal>;

View file

@ -151,7 +151,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
value={formState.description || ''} value={formState.description || ''}
onChange={e => updateForm(state => ({...state, description: e.target.value}))} onChange={e => updateForm(state => ({...state, description: e.target.value}))}
/> />
{!isFreeTier && <div className='flex gap-10'> {!isFreeTier && <div className='flex flex-col gap-10 md:flex-row'>
<div className='basis-1/2'> <div className='basis-1/2'>
<div className='mb-1 flex h-6 items-center justify-between'> <div className='mb-1 flex h-6 items-center justify-between'>
<Heading level={6}>Prices</Heading> <Heading level={6}>Prices</Heading>
@ -253,7 +253,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
</div> </div>
</Form> </Form>
</div> </div>
<div className='sticky top-[94px] shrink-0 basis-[380px]'> <div className='sticky top-[94px] hidden shrink-0 basis-[380px] min-[920px]:!visible min-[920px]:!block'>
<TierDetailPreview isFreeTier={isFreeTier} tier={formState} /> <TierDetailPreview isFreeTier={isFreeTier} tier={formState} />
</div> </div>
</div> </div>

View file

@ -16,7 +16,7 @@ interface TierCardProps {
tier: Tier; tier: Tier;
} }
const cardContainerClasses = 'group flex min-h-[200px] flex-col items-start justify-between gap-4 self-stretch rounded-sm border border-grey-300 p-4 transition-all hover:border-grey-400'; const cardContainerClasses = 'tablet:group flex min-[900px]:min-h-[200px] flex-col items-start justify-between gap-4 self-stretch rounded-sm border border-grey-300 p-4 transition-all hover:border-grey-400';
const TierCard: React.FC<TierCardProps> = ({tier}) => { const TierCard: React.FC<TierCardProps> = ({tier}) => {
const {updateRoute} = useRouting(); const {updateRoute} = useRouting();
@ -41,11 +41,11 @@ const TierCard: React.FC<TierCardProps> = ({tier}) => {
</div> </div>
{tier.monthly_price && ( {tier.monthly_price && (
tier.active ? tier.active ?
<Button className='group opacity-0 group-hover:opacity-100' color='red' label='Archive' link onClick={() => { <Button className='group group-hover:opacity-100 tablet:opacity-0' color='red' label='Archive' link onClick={() => {
updateTier({...tier, active: false}); updateTier({...tier, active: false});
}}/> }}/>
: :
<Button className='group opacity-0 group-hover:opacity-100' color='green' label='Activate' link onClick={() => { <Button className='group group-hover:opacity-100 tablet:opacity-0' color='green' label='Activate' link onClick={() => {
updateTier({...tier, active: true}); updateTier({...tier, active: true});
}}/> }}/>
)} )}
@ -71,7 +71,7 @@ const TiersList: React.FC<TiersListProps> = ({
} }
return ( return (
<div className='mt-4 grid grid-cols-3 gap-4'> <div className='mt-4 grid grid-cols-1 gap-4 min-[900px]:grid-cols-3'>
{tiers.map((tier) => { {tiers.map((tier) => {
return <TierCard tier={tier} />; return <TierCard tier={tier} />;
})} })}

View file

@ -21,23 +21,23 @@
} }
h1 { h1 {
@apply text-5xl leading-supertight; @apply text-4xl leading-supertight;
} }
h2 { h2 {
@apply text-3xl;
}
h3 {
@apply text-2xl; @apply text-2xl;
} }
h4 { h3 {
@apply text-xl; @apply text-xl;
} }
h4 {
@apply text-lg;
}
h5 { h5 {
@apply text-lg leading-tight; @apply text-md leading-supertight;
} }
h6 { h6 {

View file

@ -10,7 +10,8 @@ module.exports = {
sm: '480px', sm: '480px',
md: '640px', md: '640px',
lg: '1024px', lg: '1024px',
xl: '1280px' xl: '1280px',
tablet: '800px'
}, },
colors: { colors: {
transparent: 'transparent', transparent: 'transparent',

View file

@ -35,7 +35,8 @@ test.describe('Stripe settings', async () => {
await expect(modal).toHaveCount(0); await expect(modal).toHaveCount(0);
await expect(section.getByText('Connected to Stripe')).toHaveCount(1); // There's a mobile version of the same button in the DOM
await expect(section.getByText('Connected to Stripe')).toHaveCount(2);
// We actually do two settings update requests here, this just checks the last one // We actually do two settings update requests here, this just checks the last one
expect(lastApiRequests.editSettings?.body).toEqual({ expect(lastApiRequests.editSettings?.body).toEqual({
@ -74,7 +75,8 @@ test.describe('Stripe settings', async () => {
await expect(modal).toHaveCount(0); await expect(modal).toHaveCount(0);
await expect(section.getByText('Connected to Stripe')).toHaveCount(1); // There's a mobile version of the same button in the DOM
await expect(section.getByText('Connected to Stripe')).toHaveCount(2);
expect(lastApiRequests.editSettings?.body).toEqual({ expect(lastApiRequests.editSettings?.body).toEqual({
settings: [{ settings: [{