Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TET-904 add MetricsCard #155

Merged
merged 3 commits into from
Sep 9, 2024
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
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
Loading