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:
Sodbileg Gansukh 2024-04-29 13:00:55 +08:00 committed by GitHub
parent b2970cb4e0
commit eab5c8ba52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 88 additions and 64 deletions

View File

@ -54,3 +54,10 @@ export const WithCounter: Story = {
tabs: tabsWithCounters
}
};
export const WithTopRightContent: Story = {
args: {
tabs: tabs,
topRightContent: <p>Some content</p>
}
};

View File

@ -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) => {

View File

@ -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>
) : ''
}

View File

@ -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}

View File

@ -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();