Skip to content

Commit

Permalink
feat: APP-2619 - Implement Card and CardSummary components (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgero-eth authored Jan 8, 2024
1 parent a8ca96a commit 4a3931f
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- Implement `Card` and `CardSummary` components

### Changed

- Update `Spinner` and `Button` components to handle responsive sizes
Expand Down
33 changes: 33 additions & 0 deletions src/components/cards/card/card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Card } from './card';

const meta: Meta<typeof Card> = {
title: 'components/Cards/Card',
component: Card,
tags: ['autodocs'],
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=10157-27011&mode=design&t=2bLCEeKZ7ueBboTs-4',
},
},
};

type Story = StoryObj<typeof Card>;

/**
* Default usage example of the Card component.
*/
export const Default: Story = {
args: {
className: 'min-w-[320px]',
children: (
<div className="flex flex-col items-center p-2">
<p className="text-xs font-semibold leading-normal">Example</p>
<p className="text-xs font-normal leading-normal">Description</p>
</div>
),
},
};

export default meta;
16 changes: 16 additions & 0 deletions src/components/cards/card/card.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { render, screen } from '@testing-library/react';
import { Card, type ICardProps } from './card';

describe('<Card /> component', () => {
const createTestComponent = (props?: Partial<ICardProps>) => {
const completeProps = { ...props };

return <Card {...completeProps} />;
};

it('renders the card content', () => {
const children = 'cardContent';
render(createTestComponent({ children }));
expect(screen.getByText(children)).toBeInTheDocument();
});
});
12 changes: 12 additions & 0 deletions src/components/cards/card/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';

export interface ICardProps extends HTMLAttributes<HTMLDivElement> {}

export const Card: React.FC<ICardProps> = (props) => {
const { className, ...otherProps } = props;

return (
<div className={classNames('rounded-xl border border-neutral-100 bg-neutral-0', className)} {...otherProps} />
);
};
1 change: 1 addition & 0 deletions src/components/cards/card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Card, type ICardProps } from './card';
37 changes: 37 additions & 0 deletions src/components/cards/cardSummary/cardSummary.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { IconType } from '../../icon';
import type { ICardProps } from '../card/card';

export interface ICardSummaryAction {
/**
* Label of the summary action.
*/
label: string;
/**
* Callback called on summary action click.
*/
onClick?: () => void;
}

export interface ICardSummaryProps extends ICardProps {
/**
* Icon displayed on the card.
*/
icon: IconType;
/**
* Value of the summary.
*/
value: string;
/**
* Description of the summary.
*/
description: string;
/**
* Action of the summary.
*/
action: ICardSummaryAction;
/**
* Renders the action as stacked when set to true.
* @default true
*/
isStacked?: boolean;
}
44 changes: 44 additions & 0 deletions src/components/cards/cardSummary/cardSummary.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconType } from '../../icon';
import { CardSummary } from './cardSummary';

const meta: Meta<typeof CardSummary> = {
title: 'components/Cards/CardSummary',
component: CardSummary,
tags: ['autodocs'],
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=10157-27206&mode=dev',
},
},
};

type Story = StoryObj<typeof CardSummary>;

/**
* Default usage example of the CardSummary component.
*/
export const Default: Story = {
args: {
value: '5',
description: 'Proposals created',
action: { label: 'Create proposal', onClick: () => alert('Click') },
icon: IconType.APP_GOVERNANCE,
},
};

/**
* Set the `isStacked` property to false to render a CardSummary component with a horizontal layout (only rendered on screens > MD).
*/
export const HorizontalLayout: Story = {
args: {
value: '22',
description: 'Members',
action: { label: 'Delegate', onClick: () => alert('Click') },
icon: IconType.APP_COMMUNITY,
isStacked: false,
},
};

export default meta;
45 changes: 45 additions & 0 deletions src/components/cards/cardSummary/cardSummary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { IconType } from '../../icon';
import { CardSummary } from './cardSummary';
import type { ICardSummaryProps } from './cardSummary.api';

describe('<CardSummary /> component', () => {
const createTestComponent = (props?: Partial<ICardSummaryProps>) => {
const completeProps: ICardSummaryProps = {
value: '1',
description: 'description-test',
action: { label: 'action' },
icon: IconType.ADD,
...props,
};

return <CardSummary {...completeProps} />;
};

it('renders the summary value and description', () => {
const value = '22';
const description = 'Proposals created';
render(createTestComponent({ value, description }));
expect(screen.getByText(value)).toBeInTheDocument();
expect(screen.getByText(description)).toBeInTheDocument();
});

it('renders the specified icon', () => {
const icon = IconType.BLOCKCHAIN;
render(createTestComponent({ icon }));
expect(screen.getByTestId(icon)).toBeInTheDocument();
});

it('renders the specified action', () => {
const label = 'action-test';
const onClick = jest.fn();
const action = { label, onClick };
render(createTestComponent({ action }));

const button = screen.getByRole('button', { name: label });
expect(button).toBeInTheDocument();

fireEvent.click(button);
expect(onClick).toHaveBeenCalled();
});
});
59 changes: 59 additions & 0 deletions src/components/cards/cardSummary/cardSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import classNames from 'classnames';
import { AvatarIcon } from '../../avatars';
import { Button } from '../../button';
import { IconType } from '../../icon';
import { Card } from '../card';
import type { ICardSummaryProps } from './cardSummary.api';

export const CardSummary: React.FC<ICardSummaryProps> = (props) => {
const { icon, value, description, action, isStacked = true, className, ...otherProps } = props;

const containerClassNames = classNames(
'grid grid-cols-[auto_max-content] items-center gap-4 p-4 md:px-6 md:py-5',
{ 'grid-cols-[auto_max-content] md:gap-5': isStacked },
{
'grid-cols-[auto_max-content] md:grid-flow-col md:grid-cols-[auto_1fr_1fr_max-content] md:gap-6':
!isStacked,
},
);

return (
<Card
className={classNames(
{ 'w-[320px] md:w-[480px]': isStacked },
{ 'w-[320px] md:w-[640px]': !isStacked },
className,
)}
{...otherProps}
>
<div className={containerClassNames}>
<AvatarIcon
variant="neutral"
icon={icon}
size="md"
responsiveSize={{ md: 'lg' }}
className="self-center"
/>
<Button
variant="tertiary"
size="md"
responsiveSize={{ md: 'lg' }}
iconLeft={IconType.ADD}
onClick={action.onClick}
>
{action.label}
</Button>
<div
className={classNames(
'col-span-2 flex gap-x-2 gap-y-1',
{ 'flex-col': isStacked },
{ 'flex-col md:col-start-2 md:flex-row md:items-baseline': !isStacked },
)}
>
<p className="text-2xl font-semibold leading-tight text-neutral-800 md:text-3xl">{value}</p>
<p className="text-base font-normal leading-tight text-neutral-400">{description}</p>
</div>
</div>
</Card>
);
};
2 changes: 2 additions & 0 deletions src/components/cards/cardSummary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { CardSummary } from './cardSummary';
export { type ICardSummaryProps } from './cardSummary.api';
2 changes: 2 additions & 0 deletions src/components/cards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './card';
export * from './cardSummary';
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './actionItem';
export * from './alerts';
export * from './avatars';
export * from './button';
export * from './cards';
export * from './icon';
export * from './illustrations';
export * from './input';
Expand Down

0 comments on commit 4a3931f

Please sign in to comment.