Skip to content

Commit

Permalink
feat(sale header banner): L3-2394 create sale header banner (#360)
Browse files Browse the repository at this point in the history
  • Loading branch information
smarks8 authored Oct 10, 2024
1 parent 395b3fe commit d36d1b7
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/componentStyles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
@use 'patterns/ViewingsList/viewingsList';
@use 'patterns/Subscribe/subscribe';
@use 'patterns/Social/social';
@use 'patterns/SaleHeaderBanner/saleHeaderBanner';

// Site Furniture
@use 'site-furniture/Header/header';
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ export * from './components/Detail';
export * from './patterns/DetailList';
export * from './components/PinchZoom';
export * from './components/SeldonImage';
export * from './patterns/SaleHeaderBanner';
98 changes: 98 additions & 0 deletions src/patterns/SaleHeaderBanner/SaleHeaderBanner.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Meta } from '@storybook/react';
import SaleHeaderBanner, { SaleHeaderBannerProps } from './SaleHeaderBanner';
import { AuctionState } from './types';
import SaleHeaderCountdown from './SaleHeaderCountdown';
import SaleHeaderBrowseAuctions from './SaleHeaderBrowseAuctions';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta = {
title: 'Patterns/SaleHeaderBanner',
component: SaleHeaderBanner,
} satisfies Meta<typeof SaleHeaderBanner>;

export default meta;
export const Playground = (props: SaleHeaderBannerProps) => <SaleHeaderBanner {...props} />;

Playground.args = {
auctionTitle: 'Modern & Contemporary Art: Online Auction, New York',
occurrenceInformation: [{ occurrenceLabel: 'Begins', date: '10:00am EDT, 14 Sep 2024' }],
location: 'New York',
auctionState: AuctionState.preSale,
imageSrcUrl:
'https://assets.phillips.com/image/upload/t_Website_AuctionPageHero/v1726172550/auctions/NY090324/NY090324.jpg',
};

Playground.argTypes = {
auctionState: {
options: Object.values(AuctionState),
control: {
type: 'select',
},
},
};

export const PreSale = (props: SaleHeaderBannerProps) => (
<SaleHeaderBanner
{...props}
auctionTitle="Modern & Contemporary Art: Online Auction, New York"
imageSrcUrl="https://assets.phillips.com/image/upload/t_Website_AuctionPageHero/v1726172550/auctions/NY090324/NY090324.jpg"
occurrenceInformation={[{ date: '10:00am EDT, 4 Sep 2024', occurrenceLabel: 'Begins' }]}
location="New York"
auctionState={AuctionState.preSale}
/>
);

export const PreSaleTwoOccurrences = (props: SaleHeaderBannerProps) => (
<SaleHeaderBanner
{...props}
auctionTitle="Modern & Contemporary Art: Online Auction, New York"
imageSrcUrl="https://assets.phillips.com/image/upload/t_Website_AuctionPageHero/v1726172550/auctions/NY090324/NY090324.jpg"
occurrenceInformation={[
{ date: '10:00am EDT, 4 Sep 2024', occurrenceLabel: 'Session I' },
{ date: '10:00am EDT, 5 Sep 2024', occurrenceLabel: 'Session II' },
]}
location="New York"
auctionState={AuctionState.preSale}
/>
);

export const PreSaleThreeOccurrences = (props: SaleHeaderBannerProps) => (
<SaleHeaderBanner
{...props}
auctionTitle="Modern & Contemporary Art: Online Auction, New York"
imageSrcUrl="https://assets.phillips.com/image/upload/t_Website_AuctionPageHero/v1726172550/auctions/NY090324/NY090324.jpg"
occurrenceInformation={[
{ date: '10:00am EDT, 4 Sep 2024', occurrenceLabel: 'Session I' },
{ date: '10:00am EDT, 5 Sep 2024', occurrenceLabel: 'Session II' },
{ date: '10:00am EDT, 6 Sep 2024', occurrenceLabel: 'Session III' },
]}
location="New York"
auctionState={AuctionState.preSale}
/>
);

export const OpenForBidding = (props: SaleHeaderBannerProps) => (
<SaleHeaderBanner
{...props}
auctionTitle="Modern & Contemporary Art: Online Auction, New York"
imageSrcUrl="https://assets.phillips.com/image/upload/t_Website_AuctionPageHero/v1726172550/auctions/NY090324/NY090324.jpg"
occurrenceInformation={[{ date: '10:00am EDT, 4 Sep 2024', occurrenceLabel: 'Lots Begin to Close' }]}
location="New York"
auctionState={AuctionState.openForBidding}
>
<SaleHeaderCountdown />
</SaleHeaderBanner>
);

export const Closed = (props: SaleHeaderBannerProps) => (
<SaleHeaderBanner
{...props}
auctionTitle="Modern & Contemporary Art: Online Auction, New York"
imageSrcUrl="https://assets.phillips.com/image/upload/t_Website_AuctionPageHero/v1726172550/auctions/NY090324/NY090324.jpg"
occurrenceInformation={[{ date: '4 Sep 2024', occurrenceLabel: 'Concluded' }]}
location="New York"
auctionState={AuctionState.past}
>
<SaleHeaderBrowseAuctions />
</SaleHeaderBanner>
);
97 changes: 97 additions & 0 deletions src/patterns/SaleHeaderBanner/SaleHeaderBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import SaleHeaderBanner, { SaleHeaderBannerProps } from './SaleHeaderBanner';
import { AuctionState } from './types';
import SaleHeaderCountdown from './SaleHeaderCountdown';
import SaleHeaderBrowseAuctions from './SaleHeaderBrowseAuctions';

const defaultProps: SaleHeaderBannerProps = {
auctionTitle: 'Sample Auction',
location: 'New York',
occurrenceInformation: [{ date: '2023-12-01', occurrenceLabel: 'Auction Date' }],
auctionState: AuctionState.preSale,
imageSrcUrl: 'https://example.com/image.jpg',
};

describe('SaleHeaderBanner', () => {
it('renders the auction title', () => {
render(<SaleHeaderBanner {...defaultProps} />);
expect(screen.getByText('Sample Auction')).toBeInTheDocument();
});

it('renders the location', () => {
render(<SaleHeaderBanner {...defaultProps} />);
expect(screen.getByText('New York')).toBeInTheDocument();
});

it('renders the date and occurrence label', () => {
render(<SaleHeaderBanner {...defaultProps} />);
expect(screen.getByText('Auction Date')).toBeInTheDocument();
expect(screen.getByText('2023-12-01')).toBeInTheDocument();
});

it('renders the image with the correct src and alt attributes', () => {
render(<SaleHeaderBanner {...defaultProps} />);
const img = screen.getByAltText('Sample Auction');
expect(img).toHaveAttribute('src', 'https://example.com/image.jpg');
});

it('renders the "Register to Bid" button when auction is not closed', () => {
render(<SaleHeaderBanner {...defaultProps} />);
expect(screen.getByText('Register to Bid')).toBeInTheDocument();
});

it('does not render the "Register to Bid" button when auction is closed', () => {
render(<SaleHeaderBanner {...defaultProps} auctionState={AuctionState.past} />);
expect(screen.queryByText('Register to Bid')).not.toBeInTheDocument();
});

it('renders the countdown timer when auction is open for bidding', () => {
render(
<SaleHeaderBanner {...defaultProps} auctionState={AuctionState.openForBidding}>
<SaleHeaderCountdown />
</SaleHeaderBanner>,
);
expect(screen.getByText('Lots Close in')).toBeInTheDocument();
});

it('renders the "Browse Upcoming Sale" link when auction is closed', () => {
render(
<SaleHeaderBanner {...defaultProps} auctionState={AuctionState.past}>
<SaleHeaderBrowseAuctions />
</SaleHeaderBanner>,
);
expect(screen.getByText('Browse Upcoming Sale')).toBeInTheDocument();
expect(screen.getByText('View Calendar')).toBeInTheDocument();
});

it('renders custom CTA label when provided', () => {
render(<SaleHeaderBanner {...defaultProps} ctaLabel="Join Now" />);
expect(screen.getByText('Join Now')).toBeInTheDocument();
});

it('calls onClick handler when CTA button is clicked', () => {
const handleClick = vi.fn();
render(<SaleHeaderBanner {...defaultProps} onClick={handleClick} />);
screen.getByText('Register to Bid').click();
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('renders children when auction is open for bidding', () => {
render(
<SaleHeaderBanner {...defaultProps} auctionState={AuctionState.openForBidding}>
<div>Child Component</div>
</SaleHeaderBanner>,
);
expect(screen.getByText('Child Component')).toBeInTheDocument();
});

it('renders children when auction is closed', () => {
render(
<SaleHeaderBanner {...defaultProps} auctionState={AuctionState.past}>
<div>Child Component</div>
</SaleHeaderBanner>,
);
expect(screen.getByText('Child Component')).toBeInTheDocument();
});
});
120 changes: 120 additions & 0 deletions src/patterns/SaleHeaderBanner/SaleHeaderBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { ComponentProps, forwardRef } from 'react';
import { getCommonProps } from '../../utils';
import classnames from 'classnames';
import { SeldonImage } from '../../components/SeldonImage';
import { AuctionState } from './types';
import { Text, TextVariants } from '../../components/Text';
import { PageContentWrapper as PageMargin } from '../../components/PageContentWrapper';
import Button from '../../components/Button/Button';

// You'll need to change the ComponentProps<"htmlelementname"> to match the top-level element of your component
export interface SaleHeaderBannerProps extends ComponentProps<'div'> {
/**
* What is the title of the auction?
*/
auctionTitle: React.ReactNode;
/**
* The URL of the banner image
*/
imageSrcUrl: string;
/**
* Where is the auction taking place?
*/
location: React.ReactNode;

occurrenceInformation: {
/**
* Depending on auction state, when does the auction open or close
*/
date: React.ReactNode;
/**
* Clarifies the date based on the auction state
*/
occurrenceLabel: React.ReactNode;
}[];

/**
* What is the current state of the auction?
*/
auctionState: AuctionState;
/**
* What text should the CTA button display?
*/
ctaLabel?: React.ReactNode;
/**
* What action does the CTA take?
*/
onClick?: () => void;
}
/**
* ## Overview
*
* Sale header banner component, supports 3 states of the auction: pre-sale, open for bidding, and closed.
*
* [Figma Link](https://www.figma.com/design/OvBXAq48blO1r4qYbeBPjW/RW---Sale-Page-(PLP)?node-id=1-6&m=dev)
*
* [Storybook Link](https://phillips-seldon.netlify.app/?path=/docs/patterns-saleheaderbanner--overview)
*/

const SaleHeaderBanner = forwardRef<HTMLDivElement, SaleHeaderBannerProps>(
(
{
auctionTitle,
imageSrcUrl,
location,
auctionState,
occurrenceInformation,
ctaLabel = 'Register to Bid',
onClick,
children,
className,
...props
},
ref,
) => {
const { className: baseClassName, ...commonProps } = getCommonProps(props, 'SaleHeaderBanner');
const isOpenForBidding = auctionState === AuctionState.openForBidding;
const isClosed = auctionState === AuctionState.past;
return (
<div {...commonProps} className={classnames(baseClassName, className)} {...props} ref={ref}>
<SeldonImage
aspectRatio="16/9"
src={imageSrcUrl}
alt={String(auctionTitle)}
objectFit="cover"
className={`${baseClassName}__image`}
/>
<PageMargin className={`${baseClassName}__stack-wrapper`} {...commonProps} {...props} ref={ref}>
<div className={`${baseClassName}__stack`}>
{isOpenForBidding && children}
<Text variant={TextVariants.title1}>{auctionTitle}</Text>
<Text variant={TextVariants.string2} className={`${baseClassName}__location`}>
{location}
</Text>
<div className={`${baseClassName}__occurrence-details`}>
{occurrenceInformation.map(({ date, occurrenceLabel }) => (
<div className={`${baseClassName}__occurrence-details-text`} key={String(date)}>
<Text variant={TextVariants.string2}>{occurrenceLabel}</Text>
<Text variant={TextVariants.string2} className={`${baseClassName}__date`}>
{date}
</Text>
</div>
))}

{isClosed && children}
</div>
{!isClosed && (
<Button className={`${baseClassName}__cta`} onClick={onClick}>
{ctaLabel}
</Button>
)}
</div>
</PageMargin>
</div>
);
},
);

SaleHeaderBanner.displayName = 'SaleHeaderBanner';

export default SaleHeaderBanner;
26 changes: 26 additions & 0 deletions src/patterns/SaleHeaderBanner/SaleHeaderBrowseAuctions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ComponentProps, forwardRef } from 'react';
import { getCommonProps } from '../../utils';
import { Text, TextVariants } from '../../components/Text';
import { Link } from '../../components/Link';

export interface SaleHeaderBrowseAuctionsProps extends ComponentProps<'div'> {
ctaLabel?: string;
ctaText?: string;
}

const SaleHeaderBrowseAuctions = forwardRef<HTMLElement, SaleHeaderBrowseAuctionsProps>(
({ ctaText = 'View Calendar', ctaLabel = 'Browse Upcoming Sale', className, ...props }, _ref) => {
const { className: baseClassName } = getCommonProps(props, 'SaleHeaderBanner');

return (
<div className={`${baseClassName}__occurrence-details-text`}>
<Text variant={TextVariants.string2}>{ctaLabel}</Text>
<Link href="/calendar">{ctaText}</Link>
</div>
);
},
);

SaleHeaderBrowseAuctions.displayName = 'SaleHeaderBrowseAuctions';

export default SaleHeaderBrowseAuctions;
33 changes: 33 additions & 0 deletions src/patterns/SaleHeaderBanner/SaleHeaderCountdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ComponentProps, forwardRef } from 'react';
import { getCommonProps } from '../../utils';
import { Text, TextVariants } from '../../components/Text';

export interface SaleHeaderCountdownProps extends ComponentProps<'div'> {
label?: string;
daysLabel?: string;
hoursLabel?: string;
}

const SaleHeaderCountdown = forwardRef<HTMLDivElement, SaleHeaderCountdownProps>(
({ label = 'Lots Close in', daysLabel = 'Days', hoursLabel = 'Hours', className, ...props }, ref) => {
const { className: baseClassName, ...commonProps } = getCommonProps(props, 'SaleHeaderBanner');

return (
<div
id="PLACEHOLDER FOR TIMER COMPONENT"
className={`${baseClassName}__countdown-container`}
{...commonProps}
{...props}
ref={ref}
>
<Text variant={TextVariants.heading5}>{label}</Text>
<Text variant={TextVariants.heading5}>2 {daysLabel}</Text>
<Text variant={TextVariants.heading5}>17 {hoursLabel}</Text>
</div>
);
},
);

SaleHeaderCountdown.displayName = 'SaleHeaderCountdown';

export default SaleHeaderCountdown;
Loading

0 comments on commit d36d1b7

Please sign in to comment.