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
@ -54,3 +54,10 @@ export const WithCounter: Story = {
|
||||
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;
|
||||
border: boolean;
|
||||
buttonBorder?: boolean;
|
||||
selectedTab?: ID
|
||||
selectedTab?: ID,
|
||||
topRightContent?: React.ReactNode
|
||||
}
|
||||
|
||||
export const TabList: React.FC<TabListProps> = ({
|
||||
@ -70,7 +71,8 @@ export const TabList: React.FC<TabListProps> = ({
|
||||
handleTabChange,
|
||||
border,
|
||||
buttonBorder,
|
||||
selectedTab
|
||||
selectedTab,
|
||||
topRightContent
|
||||
}) => {
|
||||
const containerClasses = clsx(
|
||||
'no-scrollbar flex w-full overflow-x-auto',
|
||||
@ -93,6 +95,10 @@ export const TabList: React.FC<TabListProps> = ({
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{topRightContent !== null ?
|
||||
<div className='ml-auto'>{topRightContent}</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -105,6 +111,7 @@ export interface TabViewProps<ID = string> {
|
||||
buttonBorder?: boolean;
|
||||
width?: TabWidth;
|
||||
containerClassName?: string;
|
||||
topRightContent?: React.ReactNode;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
@ -116,7 +123,8 @@ function TabView<ID extends string = string>({
|
||||
border = true,
|
||||
buttonBorder = border,
|
||||
width = 'normal',
|
||||
containerClassName
|
||||
containerClassName,
|
||||
topRightContent
|
||||
}: TabViewProps<ID>) {
|
||||
if (tabs.length !== 0 && selectedTab === undefined) {
|
||||
selectedTab = tabs[0].id;
|
||||
@ -139,6 +147,7 @@ function TabView<ID extends string = string>({
|
||||
handleTabChange={handleTabChange}
|
||||
selectedTab={selectedTab}
|
||||
tabs={tabs}
|
||||
topRightContent={topRightContent}
|
||||
width={width}
|
||||
/>
|
||||
{tabs.map((tab) => {
|
||||
|
@ -77,14 +77,14 @@ const Offers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
offerButtonLink = openTiers;
|
||||
descriptionButtonText = '';
|
||||
} else if (paidActiveTiers.length > 0 && allOffers.length === 0) {
|
||||
offerButtonText = 'Add offers';
|
||||
offerButtonText = 'Add offer';
|
||||
offerButtonLink = openAddModal;
|
||||
}
|
||||
|
||||
return (
|
||||
<TopLevelGroup
|
||||
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}
|
||||
navid='offers'
|
||||
testId='offers'
|
||||
@ -115,12 +115,9 @@ const Offers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
}
|
||||
{paidActiveTiers.length === 0 && allOffers.length === 0 ?
|
||||
(<div>
|
||||
<div className='items-center-mt-1 flex justify-between'>
|
||||
<>You must have an active tier to create an offer.</>
|
||||
</div>
|
||||
<div className='items-center-mt-1 flex justify-between'>
|
||||
<Button color='green' label='Manage tiers' link linkWithPadding onClick={openTiers} />
|
||||
</div>
|
||||
<span>You must have an active tier to create an offer.</span>
|
||||
{` `}
|
||||
<Button className='font-normal' color='green' label='Manage tiers' link linkWithPadding onClick={openTiers} />
|
||||
</div>
|
||||
) : ''
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Button, Tab, TabView} 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 {NoValueLabel} 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 {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>;
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
@ -221,66 +230,68 @@ export const OffersIndexModal = () => {
|
||||
backDropClick={false}
|
||||
cancelLabel=''
|
||||
footer={false}
|
||||
header={false}
|
||||
height='full'
|
||||
size='lg'
|
||||
testId='offers-modal'
|
||||
title='Offers'
|
||||
topRightContent={<ButtonGroup buttons={buttons} />}
|
||||
width={1140}
|
||||
>
|
||||
<div className='pt-6'>
|
||||
<div className='flex h-full flex-col pt-8'>
|
||||
<header>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<TabView
|
||||
border={false}
|
||||
selectedTab={selectedTab}
|
||||
tabs={offersTabs}
|
||||
width='wide'
|
||||
onTabChange={setSelectedTab}
|
||||
/>
|
||||
</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
|
||||
direction={sortDirection as 'asc' | 'desc'}
|
||||
items={[
|
||||
{id: 'date-added', label: 'Date added', selected: sortOption === 'date-added', direction: sortDirection as 'asc' | 'desc'},
|
||||
{id: 'name', label: 'Name', selected: sortOption === 'name', direction: sortDirection as 'asc' | 'desc'},
|
||||
{id: 'redemptions', label: 'Redemptions', selected: sortOption === 'redemptions', direction: sortDirection as 'asc' | 'desc'}
|
||||
]}
|
||||
position='right'
|
||||
onDirectionChange={(selectedDirection) => {
|
||||
const newDirection = selectedDirection === 'asc' ? 'desc' : 'asc';
|
||||
setSortingState?.([{
|
||||
type: 'offers',
|
||||
option: sortOption,
|
||||
direction: newDirection
|
||||
}]);
|
||||
}}
|
||||
onSortChange={(selectedOption) => {
|
||||
setSortingState?.([{
|
||||
type: 'offers',
|
||||
option: selectedOption,
|
||||
direction: sortDirection
|
||||
}]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TabView
|
||||
selectedTab={selectedTab}
|
||||
tabs={offersTabs}
|
||||
topRightContent={
|
||||
(selectedTab === 'active' && activeOffers.length > 0) || (selectedTab === 'archived' && archivedOffers.length > 0) ?
|
||||
<div className='pt-1'>
|
||||
<SortMenu
|
||||
direction={sortDirection as 'asc' | 'desc'}
|
||||
items={[
|
||||
{id: 'date-added', label: 'Date added', selected: sortOption === 'date-added', direction: sortDirection as 'asc' | 'desc'},
|
||||
{id: 'name', label: 'Name', selected: sortOption === 'name', direction: sortDirection as 'asc' | 'desc'},
|
||||
{id: 'redemptions', label: 'Redemptions', selected: sortOption === 'redemptions', direction: sortDirection as 'asc' | 'desc'}
|
||||
]}
|
||||
position='right'
|
||||
triggerButtonProps={{
|
||||
link: true
|
||||
}}
|
||||
onDirectionChange={(selectedDirection) => {
|
||||
const newDirection = selectedDirection === 'asc' ? 'desc' : 'asc';
|
||||
setSortingState?.([{
|
||||
type: 'offers',
|
||||
option: sortOption,
|
||||
direction: newDirection
|
||||
}]);
|
||||
}}
|
||||
onSortChange={(selectedOption) => {
|
||||
setSortingState?.([{
|
||||
type: 'offers',
|
||||
option: selectedOption,
|
||||
direction: sortDirection
|
||||
}]);
|
||||
}}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
onTabChange={setSelectedTab}
|
||||
/>
|
||||
</header>
|
||||
{selectedTab === 'active' && activeOffers.length === 0 && !isFetchingOffers ?
|
||||
<NoValueLabel icon='tags-block'>
|
||||
No offers found.
|
||||
</NoValueLabel> :
|
||||
<EmptyState
|
||||
buttonAction={() => updateRoute('offers/new')}
|
||||
buttonLabel='Create an offer'
|
||||
description='Grow your audience with discounts or free trials.'
|
||||
/> :
|
||||
null
|
||||
}
|
||||
{selectedTab === 'archived' && archivedOffers.length === 0 && !isFetchingOffers ?
|
||||
<NoValueLabel icon='tags-block'>
|
||||
No offers found.
|
||||
</NoValueLabel> :
|
||||
<EmptyState
|
||||
buttonAction={() => setSelectedTab('active')}
|
||||
buttonLabel='Back to active'
|
||||
description='All archived offers will be shown here.'
|
||||
/> :
|
||||
null
|
||||
}
|
||||
{listLayoutOutput}
|
||||
|
@ -156,7 +156,7 @@ test.describe('Offers Modal', () => {
|
||||
const section = page.getByTestId('offers');
|
||||
await section.getByRole('button', {name: 'Manage offers'}).click();
|
||||
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('Second offer');
|
||||
});
|
||||
@ -175,7 +175,7 @@ test.describe('Offers Modal', () => {
|
||||
await section.getByRole('button', {name: 'Manage offers'}).click();
|
||||
const modal = page.getByTestId('offers-modal');
|
||||
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');
|
||||
});
|
||||
|
||||
@ -200,7 +200,7 @@ test.describe('Offers Modal', () => {
|
||||
const section = page.getByTestId('offers');
|
||||
await section.getByRole('button', {name: 'Manage offers'}).click();
|
||||
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 modal.getByText('First offer').click();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user