Skip to content

Commit

Permalink
feat: TET-904 add MetricsCard (#155)
Browse files Browse the repository at this point in the history
* feat: TET-904 add MetricsCard

* feat: TET-904 fix MetricsCard

* feat: TET-904 fix MetricsCard
  • Loading branch information
karolinaszarek authored Sep 9, 2024
1 parent 7250e0a commit c0ca247
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 8 deletions.
19 changes: 11 additions & 8 deletions src/components/InlineMetrics/InlineMetrics.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { IconName } from '@/utility-types/IconName';

export type InlineMetricsConfig = {
innerElements: {
label: BaseProps;
metric: BaseProps;
trendContainer: BaseProps;
trend: { trend: Partial<Record<TrendType, BaseProps>> } & BaseProps;
icon: BaseProps;
trendValue: BaseProps;
label?: BaseProps;
metric?: BaseProps;
trendContainer?: BaseProps;
trend?: { trend?: Partial<Record<TrendType, BaseProps>> } & BaseProps;
icon?: BaseProps;
trendValue?: BaseProps;
referenceDate?: BaseProps;
};
} & BaseProps;

Expand All @@ -19,6 +20,7 @@ export const defaultConfig = {
h: '',
display: 'flex',
flexDirection: 'column',
gap: '$space-component-gap-medium',
innerElements: {
trendContainer: {
display: 'flex',
Expand All @@ -28,7 +30,6 @@ export const defaultConfig = {
label: {
color: '$color-content-secondary',
text: '$typo-body-medium',
marginBottom: '$space-component-gap-medium',
},
metric: {
text: '$typo-header-4xLarge',
Expand All @@ -39,7 +40,6 @@ export const defaultConfig = {
padding: '$space-component-padding-xSmall 0',
display: 'flex',
alignItems: 'center',
alignSelf: 'flex-end',
trend: {
None: {},
Positive: {
Expand All @@ -58,6 +58,9 @@ export const defaultConfig = {
display: 'flex',
alignItems: 'end',
},
referenceDate: {
display: 'none',
},
},
} satisfies InlineMetricsConfig;

Expand Down
3 changes: 3 additions & 0 deletions src/components/InlineMetrics/InlineMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export const InlineMetrics: FC<InlineMetricsProps & MarginProps> = ({
data-testid="inline-metrics-trend-value"
>
{trendValue}
<tet.span {...styles.referenceDate} data-testid="last-year">
vs. last year
</tet.span>
</tet.span>
</tet.div>
</tet.div>
Expand Down
13 changes: 13 additions & 0 deletions src/components/MetricsCard/MetricsCard.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MetricsCardConfig } from './MetricsCard.styles';
import { InlineMetricsProps } from '../InlineMetrics';

export type IconPositionType = 'Top' | 'Left';
export type IntentType = 'Neutral' | 'Positive' | 'Negative';

export type MetricsCardProps = {
iconPosition?: IconPositionType;
hasTrend?: boolean;
hasIcon?: boolean;
hasMoreIcon?: boolean;
custom?: MetricsCardConfig;
} & Omit<InlineMetricsProps, 'custom'>;
56 changes: 56 additions & 0 deletions src/components/MetricsCard/MetricsCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Meta, StoryObj } from '@storybook/react';

import { MetricsCard } from './MetricsCard';

import { MetricsCardDocs } from '@/docs-components/MetricsCardDocs';
import { TetDocs } from '@/docs-components/TetDocs';

const meta = {
title: 'Metrics / MetricsCard',
component: MetricsCard,
tags: ['autodocs'],
args: {},
parameters: {
backgrounds: {},
docs: {
description: {
component:
'A set of several grouped components that displays numerical data, such as, for example, key performance indicators (KPIs). Metrics provide users with a clear, visual representation of essential statistics or progress.',
},
page: () => (
<TetDocs docs="https://docs.tetrisly.com/components/in-progress/metrics">
<MetricsCardDocs />
</TetDocs>
),
},
},
} satisfies Meta<typeof MetricsCard>;

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

export const Default: Story = {
args: {
trend: 'Positive',
trendValue: '+24%',
metrics: '$123.12',
label: 'Total Earnings',
hasIcon: true,
hasMoreIcon: true,
hasTrend: true,
iconPosition: 'Top',
},
};

export const IconPositionLeft: Story = {
args: {
trend: 'Negative',
trendValue: '-24%',
metrics: '$123.12',
label: 'Total Earnings',
hasIcon: true,
hasMoreIcon: true,
hasTrend: true,
iconPosition: 'Left',
},
};
75 changes: 75 additions & 0 deletions src/components/MetricsCard/MetricsCard.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { IconPositionType } from './MetricsCard.props';

import type { BaseProps } from '@/types/BaseProps';

export type MetricsCardConfig = {
iconPosition?: Record<IconPositionType, BaseProps>;
innerElements: {
trendContainer?: BaseProps;
circle?: BaseProps;
referenceDate?: BaseProps;
trend?: BaseProps;
icon?: BaseProps;
trendValue?: BaseProps;
moreIcon?: BaseProps;
};
} & BaseProps;

export const defaultConfig = {
position: 'relative',
border: '1px solid',
borderColor: '$color-border-defaultA',
borderRadius: '$border-radius-xLarge',
padding: '$space-component-padding-2xLarge',
display: 'flex',
boxShadow: '$elevation-bottom-200',
w: '480px',
iconPosition: {
Top: {
flexDirection: 'column',
},
Left: {
flexDirection: 'row',
},
},
innerElements: {
circle: {
w: '$size-large',
h: '$size-large',
padding: '$space-component-padding-medium',
border: '1px solid',
borderColor: '$color-border-neutral-subtle',
borderRadius: '24px',
},
trend: {},
icon: {
display: 'flex',
},
trendValue: {
text: '$typo-body-strong-medium',
display: 'flex',
alignItems: 'end',
},
referenceDate: {
display: 'block',
text: '$typo-body-medium',
color: '$color-content-secondary',
marginLeft: '$space-component-padding-xSmall',
},
trendContainer: {
flexDirection: 'column',
alignSelf: 'flex-start',
gap: '$space-component-gap-xLarge',
},
moreIcon: {
position: 'absolute',
color: '$color-action-neutral-normal',
top: '$space-component-padding-2xLarge',
right: '$space-component-padding-2xLarge',
},
},
} satisfies MetricsCardConfig;

export const metricsCardStyles = {
defaultConfig,
};
57 changes: 57 additions & 0 deletions src/components/MetricsCard/MetricsCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { MetricsCard } from './MetricsCard';
import { render } from '../../tests/render';

import { customPropTester } from '@/tests/customPropTester';

const getMetricsCard = (jsx: JSX.Element) => {
const { getByTestId, queryByTestId } = render(jsx);
return {
container: getByTestId('metrics-card'),
inlineMetrics: getByTestId('metrics-card-inline-metrics'),
moreIcon: queryByTestId('metrics-card-more-icon'),
walletIcon: queryByTestId('metrics-card-wallet-icon'),
};
};

describe('Metrics Card', () => {
customPropTester(
<MetricsCard
trend="None"
iconPosition="Top"
hasTrend={false}
hasIcon={false}
hasMoreIcon={false}
/>,
{
containerId: 'metrics-card',
props: {
trend: ['Negative', 'None', 'Positive'],
},
},
);

it('should render the metrics card', () => {
const { container } = getMetricsCard(<MetricsCard />);
expect(container).toBeInTheDocument();
});

it('should render the more icon', () => {
const { moreIcon } = getMetricsCard(<MetricsCard hasMoreIcon />);
expect(moreIcon).toBeInTheDocument();
});

it('should not render the more icon', () => {
const { moreIcon } = getMetricsCard(<MetricsCard />);
expect(moreIcon).toBeNull();
});

it('should render the wallet icon', () => {
const { walletIcon } = getMetricsCard(<MetricsCard hasIcon />);
expect(walletIcon).toBeInTheDocument();
});

it('should not render the wallet icon', () => {
const { walletIcon } = getMetricsCard(<MetricsCard />);
expect(walletIcon).toBeNull();
});
});
69 changes: 69 additions & 0 deletions src/components/MetricsCard/MetricsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Icon } from '@virtuslab/tetrisly-icons';
import { MarginProps } from '@xstyled/styled-components';
import { type FC, useMemo } from 'react';

import { MetricsCardProps } from './MetricsCard.props';
import { stylesBuilder } from './stylesBuilder';
import { InlineMetrics } from '../InlineMetrics';
import { InlineMetricsConfig } from '../InlineMetrics/InlineMetrics.styles';

import { tet } from '@/tetrisly';

export const MetricsCard: FC<MetricsCardProps & MarginProps> = ({
hasIcon = false,
hasMoreIcon = false,
hasTrend,
metrics,
label,
trend = 'None',
trendValue,
iconPosition = 'Top',
custom,
...restProps
}) => {
const styles = useMemo(
() => stylesBuilder({ iconPosition, custom }),
[custom, iconPosition],
);
const isLeftPosition = iconPosition === 'Left';

const customInlineMetrics: InlineMetricsConfig = {
gap: '$space-component-gap-null',
innerElements: {
trendContainer: { ...styles.trendContainer },
referenceDate: { ...styles.referenceDate },
trend: { ...styles.trend, display: hasTrend ? 'flex' : 'none' },
},
};

return (
<tet.div {...styles.container} data-testid="metrics-card" {...restProps}>
{hasIcon && (
<tet.div
marginBottom={isLeftPosition ? 0 : '$space-component-gap-xLarge'}
marginRight={isLeftPosition ? '$space-component-gap-xLarge' : 0}
{...styles.circle}
data-testid="metrics-card-circle"
>
<Icon data-testid="metrics-card-wallet-icon" name="20-wallet" />
</tet.div>
)}
<InlineMetrics
metrics={metrics}
label={label}
trend={trend}
trendValue={trendValue}
custom={customInlineMetrics}
data-testid="metrics-card-inline-metrics"
/>
{hasMoreIcon && (
<tet.div {...styles.moreIcon} data-testid="metrics-card-more-icon">
<Icon
data-testid="metrics-card-more-icon-svg"
name="20-more-horizontal"
/>
</tet.div>
)}
</tet.div>
);
};
3 changes: 3 additions & 0 deletions src/components/MetricsCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { MetricsCard } from './MetricsCard';
export type { MetricsCardProps } from './MetricsCard.props';
export { metricsCardStyles } from './MetricsCard.styles';
30 changes: 30 additions & 0 deletions src/components/MetricsCard/stylesBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { IconPositionType, MetricsCardProps } from './MetricsCard.props';
import { defaultConfig } from './MetricsCard.styles';

import { mergeConfigWithCustom } from '@/services';

type StylesBuilderParams = {
iconPosition: IconPositionType;
custom: MetricsCardProps['custom'];
};

export const stylesBuilder = ({
iconPosition,
custom,
}: StylesBuilderParams) => {
const {
innerElements,
iconPosition: position,
...restStyles
} = mergeConfigWithCustom({
defaultConfig,
custom,
});

const containerStyles = { ...restStyles, ...position[iconPosition] };

return {
container: containerStyles,
...innerElements,
};
};
Loading

0 comments on commit c0ca247

Please sign in to comment.