0
Fork 0
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:
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 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; 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) => {

View file

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

View file

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

View file

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