From 03dee22ae79cd9adce5ac3c55f1a0cadd1bedc9a Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Wed, 13 Dec 2023 17:27:41 +0100 Subject: [PATCH] feat: APP-2619 - Implement Card and CardSummary components --- CHANGELOG.md | 4 ++ src/components/cards/card/card.stories.tsx | 33 +++++++++++ src/components/cards/card/card.test.tsx | 16 +++++ src/components/cards/card/card.tsx | 12 ++++ src/components/cards/card/index.ts | 1 + .../cards/cardSummary/cardSummary.api.ts | 37 ++++++++++++ .../cards/cardSummary/cardSummary.stories.tsx | 44 ++++++++++++++ .../cards/cardSummary/cardSummary.test.tsx | 45 ++++++++++++++ .../cards/cardSummary/cardSummary.tsx | 59 +++++++++++++++++++ src/components/cards/cardSummary/index.ts | 2 + src/components/cards/index.ts | 2 + src/components/index.ts | 1 + 12 files changed, 256 insertions(+) create mode 100644 src/components/cards/card/card.stories.tsx create mode 100644 src/components/cards/card/card.test.tsx create mode 100644 src/components/cards/card/card.tsx create mode 100644 src/components/cards/card/index.ts create mode 100644 src/components/cards/cardSummary/cardSummary.api.ts create mode 100644 src/components/cards/cardSummary/cardSummary.stories.tsx create mode 100644 src/components/cards/cardSummary/cardSummary.test.tsx create mode 100644 src/components/cards/cardSummary/cardSummary.tsx create mode 100644 src/components/cards/cardSummary/index.ts create mode 100644 src/components/cards/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 526d151cb..f87aa251b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/components/cards/card/card.stories.tsx b/src/components/cards/card/card.stories.tsx new file mode 100644 index 000000000..a6afae5f6 --- /dev/null +++ b/src/components/cards/card/card.stories.tsx @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Card } from './card'; + +const meta: Meta = { + 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; + +/** + * Default usage example of the Card component. + */ +export const Default: Story = { + args: { + className: 'min-w-[320px]', + children: ( +
+

Example

+

Description

+
+ ), + }, +}; + +export default meta; diff --git a/src/components/cards/card/card.test.tsx b/src/components/cards/card/card.test.tsx new file mode 100644 index 000000000..cec1b7acb --- /dev/null +++ b/src/components/cards/card/card.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from '@testing-library/react'; +import { Card, type ICardProps } from './card'; + +describe(' component', () => { + const createTestComponent = (props?: Partial) => { + const completeProps = { ...props }; + + return ; + }; + + it('renders the card content', () => { + const children = 'cardContent'; + render(createTestComponent({ children })); + expect(screen.getByText(children)).toBeInTheDocument(); + }); +}); diff --git a/src/components/cards/card/card.tsx b/src/components/cards/card/card.tsx new file mode 100644 index 000000000..d4d598e18 --- /dev/null +++ b/src/components/cards/card/card.tsx @@ -0,0 +1,12 @@ +import classNames from 'classnames'; +import type { HTMLAttributes } from 'react'; + +export interface ICardProps extends HTMLAttributes {} + +export const Card: React.FC = (props) => { + const { className, ...otherProps } = props; + + return ( +
+ ); +}; diff --git a/src/components/cards/card/index.ts b/src/components/cards/card/index.ts new file mode 100644 index 000000000..5fd02055d --- /dev/null +++ b/src/components/cards/card/index.ts @@ -0,0 +1 @@ +export { Card, type ICardProps } from './card'; diff --git a/src/components/cards/cardSummary/cardSummary.api.ts b/src/components/cards/cardSummary/cardSummary.api.ts new file mode 100644 index 000000000..211d9bc72 --- /dev/null +++ b/src/components/cards/cardSummary/cardSummary.api.ts @@ -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; +} diff --git a/src/components/cards/cardSummary/cardSummary.stories.tsx b/src/components/cards/cardSummary/cardSummary.stories.tsx new file mode 100644 index 000000000..b54eac6b0 --- /dev/null +++ b/src/components/cards/cardSummary/cardSummary.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { IconType } from '../../icon'; +import { CardSummary } from './cardSummary'; + +const meta: Meta = { + 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; + +/** + * 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; diff --git a/src/components/cards/cardSummary/cardSummary.test.tsx b/src/components/cards/cardSummary/cardSummary.test.tsx new file mode 100644 index 000000000..7a90a8b97 --- /dev/null +++ b/src/components/cards/cardSummary/cardSummary.test.tsx @@ -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(' component', () => { + const createTestComponent = (props?: Partial) => { + const completeProps: ICardSummaryProps = { + value: '1', + description: 'description-test', + action: { label: 'action' }, + icon: IconType.ADD, + ...props, + }; + + return ; + }; + + 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(); + }); +}); diff --git a/src/components/cards/cardSummary/cardSummary.tsx b/src/components/cards/cardSummary/cardSummary.tsx new file mode 100644 index 000000000..f98b09d31 --- /dev/null +++ b/src/components/cards/cardSummary/cardSummary.tsx @@ -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 = (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 ( + +
+ + +
+

{value}

+

{description}

+
+
+
+ ); +}; diff --git a/src/components/cards/cardSummary/index.ts b/src/components/cards/cardSummary/index.ts new file mode 100644 index 000000000..c2dbb6702 --- /dev/null +++ b/src/components/cards/cardSummary/index.ts @@ -0,0 +1,2 @@ +export { CardSummary } from './cardSummary'; +export { type ICardSummaryProps } from './cardSummary.api'; diff --git a/src/components/cards/index.ts b/src/components/cards/index.ts new file mode 100644 index 000000000..58acd853e --- /dev/null +++ b/src/components/cards/index.ts @@ -0,0 +1,2 @@ +export * from './card'; +export * from './cardSummary'; diff --git a/src/components/index.ts b/src/components/index.ts index c7f90e12e..39110c12e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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';