Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed various design issues in Offers #19922

Merged
merged 6 commits into from Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/admin-x-design-system/src/global/TabView.stories.tsx
Expand Up @@ -54,3 +54,10 @@ export const WithCounter: Story = {
tabs: tabsWithCounters
}
};

export const WithTopRightContent: Story = {
args: {
tabs: tabs,
topRightContent: <p>Some content</p>
}
};
15 changes: 12 additions & 3 deletions apps/admin-x-design-system/src/global/TabView.tsx
Expand Up @@ -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> = ({
Expand All @@ -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',
Expand All @@ -93,6 +95,10 @@ export const TabList: React.FC<TabListProps> = ({
/>
</div>
))}
{topRightContent !== null ?
<div className='ml-auto'>{topRightContent}</div> :
null
}
</div>
);
};
Expand All @@ -105,6 +111,7 @@ export interface TabViewProps<ID = string> {
buttonBorder?: boolean;
width?: TabWidth;
containerClassName?: string;
topRightContent?: React.ReactNode;
testId?: string;
}

Expand All @@ -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;
Expand All @@ -139,6 +147,7 @@ function TabView<ID extends string = string>({
handleTabChange={handleTabChange}
selectedTab={selectedTab}
tabs={tabs}
topRightContent={topRightContent}
width={width}
/>
{tabs.map((tab) => {
Expand Down
13 changes: 5 additions & 8 deletions apps/admin-x-settings/src/components/settings/growth/Offers.tsx
Expand Up @@ -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'
Expand Down Expand Up @@ -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>
) : ''
}
Expand Down
@@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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}
Expand Down
Expand Up @@ -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');
});
Expand All @@ -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');
});

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

Expand Down