Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/components/card/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ReactNode } from 'react';

/**
* Card component
*
* @property {string} [className]
* - Optional additional CSS classes to customize the appearance of the card.
* By default, the card has rounded corners, border, padding, and shadow.
*
* @property {ReactNode} children
* - The content to be rendered inside the card. This can be any valid React node.
*/

const Card = ({ className = '', children }: { className?: string; children: ReactNode }): JSX.Element => {
return (
<div
className={`rounded-xl border border-gray-10 bg-surface p-5 shadow-[0_12px_20px_0_rgba(0,0,0,0.02)] dark:bg-gray-1 ${className}`}
>
{children}
</div>
);
};

export default Card;
40 changes: 40 additions & 0 deletions src/components/card/__test__/Card.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { render } from '@testing-library/react';
import Card from '../Card';
import { describe, expect, it } from 'vitest';

describe('Card Component', () => {
const renderCard = (props = {}) =>
render(
<Card {...props}>
<h3>Test Card</h3>
<p>Card content</p>
</Card>,
);

it('should match snapshot', () => {
const card = renderCard();
expect(card).toMatchSnapshot();
});

it('should render the content passed as children', () => {
const { getByText } = renderCard();
expect(getByText('Test Card')).toBeInTheDocument();
expect(getByText('Card content')).toBeInTheDocument();
});

it('should apply the default className', () => {
const { container } = renderCard();
expect(container.firstChild).toHaveClass(
'rounded-xl border border-gray-10 bg-surface p-5 shadow-[0_12px_20px_0_rgba(0,0,0,0.02)] dark:bg-gray-1',
);
});

it('should append custom className to the default styles', () => {
const { container } = renderCard({ className: 'custom-class' });
expect(container.firstChild).toHaveClass('custom-class');
expect(container.firstChild).toHaveClass(
'rounded-xl border border-gray-10 bg-surface p-5 shadow-[0_12px_20px_0_rgba(0,0,0,0.02)] dark:bg-gray-1',
);
});
});
84 changes: 84 additions & 0 deletions src/components/card/__test__/__snapshots__/Card.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Card Component > should match snapshot 1`] = `
{
"asFragment": [Function],
"baseElement": <body>
<div>
<div
class="rounded-xl border border-gray-10 bg-surface p-5 shadow-[0_12px_20px_0_rgba(0,0,0,0.02)] dark:bg-gray-1 "
>
<h3>
Test Card
</h3>
<p>
Card content
</p>
</div>
</div>
</body>,
"container": <div>
<div
class="rounded-xl border border-gray-10 bg-surface p-5 shadow-[0_12px_20px_0_rgba(0,0,0,0.02)] dark:bg-gray-1 "
>
<h3>
Test Card
</h3>
<p>
Card content
</p>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './radio-button/RadioButton';
export * from './avatar/Avatar';
export * from './slider/RangeSlider';
export * from './dialog/Dialog';
export * from './card/Card';
export * from './empty/Empty';
export * from './modal/Modal';
export * from './popover/Popover';
Expand Down
38 changes: 38 additions & 0 deletions src/stories/components/card/Card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from '@storybook/react';
import Card from '../../../components/card/Card';

const meta: Meta<typeof Card> = {
title: 'Components/Card',
component: Card,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
};

export default meta;
type Story = StoryObj<typeof Card>;

export const Default: Story = {
args: {
children: (
<div>
<h3 className="text-xl font-semibold">Default Card</h3>
<p className="text-gray-700">This is a simple card.</p>
</div>
),
className: '',
},
};

export const CustomStyledCard: Story = {
args: {
children: (
<div>
<h3 className="text-xl font-semibold text-primary">Custom Card</h3>
<p className="text-gray-500">This card has custom styles.</p>
</div>
),
className: 'bg-primary/10 border-primary/20',
},
};