mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
Fixed various design issues in Offers (#19922)
ref DES-101 - used the default TabView component on Offers list for better consistency - added new property to TabView component which makes it possible to have extra content on the top right - updated copy of the empty states
This commit is contained in:
parent
b2970cb4e0
commit
eab5c8ba52
5 changed files with 88 additions and 64 deletions
|
@ -54,3 +54,10 @@ export const WithCounter: Story = {
|
||||||
tabs: tabsWithCounters
|
tabs: tabsWithCounters
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithTopRightContent: Story = {
|
||||||
|
args: {
|
||||||
|
tabs: tabs,
|
||||||
|
topRightContent: <p>Some content</p>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -61,7 +61,8 @@ export interface TabListProps<ID = string> {
|
||||||
handleTabChange?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
handleTabChange?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
border: boolean;
|
border: boolean;
|
||||||
buttonBorder?: boolean;
|
buttonBorder?: boolean;
|
||||||
selectedTab?: ID
|
selectedTab?: ID,
|
||||||
|
topRightContent?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabList: React.FC<TabListProps> = ({
|
export const TabList: React.FC<TabListProps> = ({
|
||||||
|
@ -70,7 +71,8 @@ export const TabList: React.FC<TabListProps> = ({
|
||||||
handleTabChange,
|
handleTabChange,
|
||||||
border,
|
border,
|
||||||
buttonBorder,
|
buttonBorder,
|
||||||
selectedTab
|
selectedTab,
|
||||||
|
topRightContent
|
||||||
}) => {
|
}) => {
|
||||||
const containerClasses = clsx(
|
const containerClasses = clsx(
|
||||||
'no-scrollbar flex w-full overflow-x-auto',
|
'no-scrollbar flex w-full overflow-x-auto',
|
||||||
|
@ -93,6 +95,10 @@ export const TabList: React.FC<TabListProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{topRightContent !== null ?
|
||||||
|
<div className='ml-auto'>{topRightContent}</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -105,6 +111,7 @@ export interface TabViewProps<ID = string> {
|
||||||
buttonBorder?: boolean;
|
buttonBorder?: boolean;
|
||||||
width?: TabWidth;
|
width?: TabWidth;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
topRightContent?: React.ReactNode;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +123,8 @@ function TabView<ID extends string = string>({
|
||||||
border = true,
|
border = true,
|
||||||
buttonBorder = border,
|
buttonBorder = border,
|
||||||
width = 'normal',
|
width = 'normal',
|
||||||
containerClassName
|
containerClassName,
|
||||||
|
topRightContent
|
||||||
}: TabViewProps<ID>) {
|
}: TabViewProps<ID>) {
|
||||||
if (tabs.length !== 0 && selectedTab === undefined) {
|
if (tabs.length !== 0 && selectedTab === undefined) {
|
||||||
selectedTab = tabs[0].id;
|
selectedTab = tabs[0].id;
|
||||||
|
@ -139,6 +147,7 @@ function TabView<ID extends string = string>({
|
||||||
handleTabChange={handleTabChange}
|
handleTabChange={handleTabChange}
|
||||||
selectedTab={selectedTab}
|
selectedTab={selectedTab}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
|
topRightContent={topRightContent}
|
||||||
width={width}
|
width={width}
|
||||||
/>
|
/>
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
|
|
|
@ -77,14 +77,14 @@ const Offers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||||
offerButtonLink = openTiers;
|
offerButtonLink = openTiers;
|
||||||
descriptionButtonText = '';
|
descriptionButtonText = '';
|
||||||
} else if (paidActiveTiers.length > 0 && allOffers.length === 0) {
|
} else if (paidActiveTiers.length > 0 && allOffers.length === 0) {
|
||||||
offerButtonText = 'Add offers';
|
offerButtonText = 'Add offer';
|
||||||
offerButtonLink = openAddModal;
|
offerButtonLink = openAddModal;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopLevelGroup
|
<TopLevelGroup
|
||||||
customButtons={<Button color='green' disabled={!checkStripeEnabled(settings, config)} label={offerButtonText} link linkWithPadding onClick={offerButtonLink}/>}
|
customButtons={<Button color='green' disabled={!checkStripeEnabled(settings, config)} label={offerButtonText} link linkWithPadding onClick={offerButtonLink}/>}
|
||||||
description={<>Create discounts & coupons to boost new subscriptions. {allOffers.length === 0 && <><br /><a className='text-green' href="https://ghost.org/help/offers" rel="noopener noreferrer" target="_blank">{descriptionButtonText}</a></>}</>}
|
description={<>Create discounts & coupons to boost new subscriptions. {allOffers.length === 0 && <><a className='text-green' href="https://ghost.org/help/offers" rel="noopener noreferrer" target="_blank">{descriptionButtonText}</a></>}</>}
|
||||||
keywords={keywords}
|
keywords={keywords}
|
||||||
navid='offers'
|
navid='offers'
|
||||||
testId='offers'
|
testId='offers'
|
||||||
|
@ -115,12 +115,9 @@ const Offers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||||
}
|
}
|
||||||
{paidActiveTiers.length === 0 && allOffers.length === 0 ?
|
{paidActiveTiers.length === 0 && allOffers.length === 0 ?
|
||||||
(<div>
|
(<div>
|
||||||
<div className='items-center-mt-1 flex justify-between'>
|
<span>You must have an active tier to create an offer.</span>
|
||||||
<>You must have an active tier to create an offer.</>
|
{` `}
|
||||||
</div>
|
<Button className='font-normal' color='green' label='Manage tiers' link linkWithPadding onClick={openTiers} />
|
||||||
<div className='items-center-mt-1 flex justify-between'>
|
|
||||||
<Button color='green' label='Manage tiers' link linkWithPadding onClick={openTiers} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : ''
|
) : ''
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Button, Tab, TabView} from '@tryghost/admin-x-design-system';
|
import {Button, Tab, TabView} from '@tryghost/admin-x-design-system';
|
||||||
import {ButtonGroup, ButtonProps, showToast} from '@tryghost/admin-x-design-system';
|
import {ButtonGroup, ButtonProps, showToast} from '@tryghost/admin-x-design-system';
|
||||||
|
import {Icon} from '@tryghost/admin-x-design-system';
|
||||||
import {Modal} from '@tryghost/admin-x-design-system';
|
import {Modal} from '@tryghost/admin-x-design-system';
|
||||||
import {NoValueLabel} from '@tryghost/admin-x-design-system';
|
|
||||||
import {SortMenu} from '@tryghost/admin-x-design-system';
|
import {SortMenu} from '@tryghost/admin-x-design-system';
|
||||||
import {Tier, getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
|
import {Tier, getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
|
||||||
import {Tooltip} from '@tryghost/admin-x-design-system';
|
import {Tooltip} from '@tryghost/admin-x-design-system';
|
||||||
|
@ -90,6 +90,15 @@ export const CopyLinkButton: React.FC<{offerCode: string}> = ({offerCode}) => {
|
||||||
return <Tooltip containerClassName='group-hover:opacity-100 opacity-0 inline-flex items-center -mr-1 justify-center leading-none w-5 h-5' content={isCopied ? 'Copied' : 'Copy link'} size='sm'><Button color='clear' hideLabel={true} icon={isCopied ? 'check-circle' : 'hyperlink-circle'} iconColorClass={isCopied ? 'text-green w-[14px] h-[14px]' : 'w-[18px] h-[18px]'} label={isCopied ? 'Copied' : 'Copy'} unstyled={true} onClick={handleCopyClick} /></Tooltip>;
|
return <Tooltip containerClassName='group-hover:opacity-100 opacity-0 inline-flex items-center -mr-1 justify-center leading-none w-5 h-5' content={isCopied ? 'Copied' : 'Copy link'} size='sm'><Button color='clear' hideLabel={true} icon={isCopied ? 'check-circle' : 'hyperlink-circle'} iconColorClass={isCopied ? 'text-green w-[14px] h-[14px]' : 'w-[18px] h-[18px]'} label={isCopied ? 'Copied' : 'Copy'} unstyled={true} onClick={handleCopyClick} /></Tooltip>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EmptyState: React.FC<{title?: string, description: string, buttonAction: () => void, buttonLabel: string}> = ({title = 'No offers found', description, buttonAction, buttonLabel}) => (
|
||||||
|
<div className='flex h-full grow flex-col items-center justify-center text-center'>
|
||||||
|
<Icon className='-mt-14' colorClass='text-grey-700 -mt-6' name='tags-block' size='xl' />
|
||||||
|
<h1 className='mt-6 text-2xl'>{title}</h1>
|
||||||
|
<p className='mt-3 max-w-[420px] text-[1.6rem]'>{description}</p>
|
||||||
|
<Button className="mt-8" color="grey" label={buttonLabel} onClick={buttonAction}></Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const OffersIndexModal = () => {
|
export const OffersIndexModal = () => {
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const {updateRoute} = useRouting();
|
const {updateRoute} = useRouting();
|
||||||
|
@ -221,29 +230,21 @@ export const OffersIndexModal = () => {
|
||||||
backDropClick={false}
|
backDropClick={false}
|
||||||
cancelLabel=''
|
cancelLabel=''
|
||||||
footer={false}
|
footer={false}
|
||||||
header={false}
|
|
||||||
height='full'
|
height='full'
|
||||||
size='lg'
|
size='lg'
|
||||||
testId='offers-modal'
|
testId='offers-modal'
|
||||||
|
title='Offers'
|
||||||
|
topRightContent={<ButtonGroup buttons={buttons} />}
|
||||||
width={1140}
|
width={1140}
|
||||||
>
|
>
|
||||||
<div className='pt-6'>
|
<div className='flex h-full flex-col pt-8'>
|
||||||
<header>
|
<header>
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<div>
|
|
||||||
<TabView
|
<TabView
|
||||||
border={false}
|
|
||||||
selectedTab={selectedTab}
|
selectedTab={selectedTab}
|
||||||
tabs={offersTabs}
|
tabs={offersTabs}
|
||||||
width='wide'
|
topRightContent={
|
||||||
onTabChange={setSelectedTab}
|
(selectedTab === 'active' && activeOffers.length > 0) || (selectedTab === 'archived' && archivedOffers.length > 0) ?
|
||||||
/>
|
<div className='pt-1'>
|
||||||
</div>
|
|
||||||
<ButtonGroup buttons={buttons} />
|
|
||||||
</div>
|
|
||||||
<div className='mt-12 flex items-center justify-between border-b border-b-grey-300 pb-2.5 dark:border-b-grey-800'>
|
|
||||||
<h1 className='text-3xl'>{offersTabs.find(tab => tab.id === selectedTab)?.title} offers</h1>
|
|
||||||
<div>
|
|
||||||
<SortMenu
|
<SortMenu
|
||||||
direction={sortDirection as 'asc' | 'desc'}
|
direction={sortDirection as 'asc' | 'desc'}
|
||||||
items={[
|
items={[
|
||||||
|
@ -252,6 +253,9 @@ export const OffersIndexModal = () => {
|
||||||
{id: 'redemptions', label: 'Redemptions', selected: sortOption === 'redemptions', direction: sortDirection as 'asc' | 'desc'}
|
{id: 'redemptions', label: 'Redemptions', selected: sortOption === 'redemptions', direction: sortDirection as 'asc' | 'desc'}
|
||||||
]}
|
]}
|
||||||
position='right'
|
position='right'
|
||||||
|
triggerButtonProps={{
|
||||||
|
link: true
|
||||||
|
}}
|
||||||
onDirectionChange={(selectedDirection) => {
|
onDirectionChange={(selectedDirection) => {
|
||||||
const newDirection = selectedDirection === 'asc' ? 'desc' : 'asc';
|
const newDirection = selectedDirection === 'asc' ? 'desc' : 'asc';
|
||||||
setSortingState?.([{
|
setSortingState?.([{
|
||||||
|
@ -268,19 +272,26 @@ export const OffersIndexModal = () => {
|
||||||
}]);
|
}]);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> :
|
||||||
</div>
|
null
|
||||||
|
}
|
||||||
|
onTabChange={setSelectedTab}
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
{selectedTab === 'active' && activeOffers.length === 0 && !isFetchingOffers ?
|
{selectedTab === 'active' && activeOffers.length === 0 && !isFetchingOffers ?
|
||||||
<NoValueLabel icon='tags-block'>
|
<EmptyState
|
||||||
No offers found.
|
buttonAction={() => updateRoute('offers/new')}
|
||||||
</NoValueLabel> :
|
buttonLabel='Create an offer'
|
||||||
|
description='Grow your audience with discounts or free trials.'
|
||||||
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
{selectedTab === 'archived' && archivedOffers.length === 0 && !isFetchingOffers ?
|
{selectedTab === 'archived' && archivedOffers.length === 0 && !isFetchingOffers ?
|
||||||
<NoValueLabel icon='tags-block'>
|
<EmptyState
|
||||||
No offers found.
|
buttonAction={() => setSelectedTab('active')}
|
||||||
</NoValueLabel> :
|
buttonLabel='Back to active'
|
||||||
|
description='All archived offers will be shown here.'
|
||||||
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
{listLayoutOutput}
|
{listLayoutOutput}
|
||||||
|
|
|
@ -156,7 +156,7 @@ test.describe('Offers Modal', () => {
|
||||||
const section = page.getByTestId('offers');
|
const section = page.getByTestId('offers');
|
||||||
await section.getByRole('button', {name: 'Manage offers'}).click();
|
await section.getByRole('button', {name: 'Manage offers'}).click();
|
||||||
const modal = page.getByTestId('offers-modal');
|
const modal = page.getByTestId('offers-modal');
|
||||||
await expect(modal).toContainText('Active offers');
|
await expect(modal.getByText('Active')).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(modal).toContainText('First offer');
|
await expect(modal).toContainText('First offer');
|
||||||
await expect(modal).toContainText('Second offer');
|
await expect(modal).toContainText('Second offer');
|
||||||
});
|
});
|
||||||
|
@ -175,7 +175,7 @@ test.describe('Offers Modal', () => {
|
||||||
await section.getByRole('button', {name: 'Manage offers'}).click();
|
await section.getByRole('button', {name: 'Manage offers'}).click();
|
||||||
const modal = page.getByTestId('offers-modal');
|
const modal = page.getByTestId('offers-modal');
|
||||||
await modal.getByText('Archived').click();
|
await modal.getByText('Archived').click();
|
||||||
await expect(modal).toContainText('Archived offers');
|
await expect(modal.getByText('Archived')).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(modal).toContainText('Third offer');
|
await expect(modal).toContainText('Third offer');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ test.describe('Offers Modal', () => {
|
||||||
const section = page.getByTestId('offers');
|
const section = page.getByTestId('offers');
|
||||||
await section.getByRole('button', {name: 'Manage offers'}).click();
|
await section.getByRole('button', {name: 'Manage offers'}).click();
|
||||||
const modal = page.getByTestId('offers-modal');
|
const modal = page.getByTestId('offers-modal');
|
||||||
await expect(modal).toContainText('Active offers');
|
await expect(modal.getByText('Active')).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(modal).toContainText('First offer');
|
await expect(modal).toContainText('First offer');
|
||||||
await modal.getByText('First offer').click();
|
await modal.getByText('First offer').click();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue