Skip to content

Commit 985fa0f

Browse files
authored
feat: add TxCard component and related (#37)
⭐ Closes FRO-471 ⭐ Closes FRO-472 ⭐ Closes FRO-473
1 parent 7906d92 commit 985fa0f

19 files changed

+9769
-102
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,20 @@
5353
"@fuels/jest": "^0.0.9",
5454
"@fuels/prettier-config": "^0.0.9",
5555
"@fuels/ts-config": "^0.0.9",
56-
"@fuels/tsup-config": "^0.0.9",
5756
"jest": "29.7.0",
5857
"lint-staged": "14.0.1"
5958
},
6059
"devDependencies": {
60+
"@fuels/tsup-config": "^0.0.9",
6161
"@swc/core": "1.3.84",
6262
"@swc/jest": "0.2.29",
6363
"@types/jest": "29.5.4",
64+
"@types/node": "20.6.0",
6465
"eslint": "^8.49.0",
6566
"husky": "^8.0.3",
6667
"npm-run-all": "^4.1.5",
6768
"prettier": "^3.0.3",
69+
"tsup": "7.2.0",
6870
"turbo": "^1.10.13",
6971
"typescript": "5.2.2",
7072
"updates": "^15.0.2"

packages/app/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
"ts:check": "tsc --noEmit"
1717
},
1818
"dependencies": {
19+
"@fuel-explorer/graphql": "workspace:*",
1920
"@fuel-ui/css": "0.21.0-pr-305-04b8434",
2021
"@fuel-ui/icons": "0.21.0-pr-305-04b8434",
2122
"@fuel-ui/react": "0.21.0-pr-305-04b8434",
2223
"csstype": "3.1.2",
24+
"dayjs": "1.11.9",
25+
"fuels": "0.57.0",
2326
"next": "13.4.19",
2427
"react": "18.2.0",
2528
"react-dom": "18.2.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import dayjs from 'dayjs';
2+
import relativeTime from 'dayjs/plugin/relativeTime';
3+
dayjs.extend(relativeTime);
4+
5+
export function fromNow(timestamp: string) {
6+
return dayjs.unix(Number(timestamp)).fromNow();
7+
}
8+
9+
export function unixTimestamp(timestamp: number) {
10+
return dayjs(timestamp).unix().toString();
11+
}
12+
13+
export { dayjs };

packages/app/src/systems/Home/pages/HomePage.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { Box, Icon } from '@fuel-ui/react';
3+
import { Box, Link } from '@fuel-ui/react';
44

55
export function HomePage() {
66
return (
@@ -12,8 +12,7 @@ export function HomePage() {
1212
is: ['centered'],
1313
}}
1414
>
15-
<Icon icon="Calendar" />
16-
Hello World
15+
<Link href="/storybook">Go to Storybook</Link>
1716
</Box.Flex>
1817
);
1918
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
3+
import { TX_CONTRACT_CALL_MOCK } from '../__mocks__/tx';
4+
5+
import { TxCard } from './TxCard';
6+
7+
const meta: Meta<typeof TxCard> = {
8+
title: 'Transaction/TxCard',
9+
component: TxCard,
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof TxCard>;
14+
15+
export const Usage: Story = {
16+
render: () => <TxCard tx={TX_CONTRACT_CALL_MOCK} css={{ maxW: 300 }} />,
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { cssObj } from '@fuel-ui/css';
2+
import type { BaseProps } from '@fuel-ui/react';
3+
import { Box, Card, Text } from '@fuel-ui/react';
4+
import { bn } from 'fuels';
5+
import { fromNow } from '~/systems/Core/utils/dayjs';
6+
7+
import type { TxItem } from '../../types';
8+
import { TxTitle } from '../TxTitle/TxTitle';
9+
10+
type TxCardProps = BaseProps<{
11+
tx: TxItem;
12+
}>;
13+
14+
export function TxCard({ tx, css, ...props }: TxCardProps) {
15+
return (
16+
<Card {...props} css={{ ...styles.root, ...css }}>
17+
<TxTitle
18+
type={tx.type}
19+
status={tx.status}
20+
txHash={tx.transaction.id}
21+
css={styles.title}
22+
/>
23+
<Box.VStack css={styles.body}>
24+
<Box.Flex justify="between">
25+
<Text leftIcon="Users">4 accounts</Text>
26+
</Box.Flex>
27+
<Box.Flex justify="between" css={styles.row}>
28+
<Text leftIcon="Transfer">{tx.totalOperations} operations</Text>
29+
<Text leftIcon="ClockHour1" className="small">
30+
{fromNow(tx.timestamp)}
31+
</Text>
32+
</Box.Flex>
33+
<Box.Flex justify="between" css={styles.row}>
34+
<Text leftIcon="Coins">{tx.totalAssets} assets</Text>
35+
<Text leftIcon="GasStation" className="small">
36+
{bn(tx.gasUsed).format({ units: 3 })} ETH
37+
</Text>
38+
</Box.Flex>
39+
</Box.VStack>
40+
</Card>
41+
);
42+
}
43+
44+
const styles = {
45+
root: cssObj({
46+
transition: 'all 0.2s ease-out',
47+
48+
'&:hover': {
49+
borderColor: '$borderHover',
50+
},
51+
}),
52+
title: cssObj({
53+
py: '$4',
54+
px: '$4',
55+
}),
56+
body: cssObj({
57+
borderTop: '1px solid $cardBorder',
58+
py: '$4',
59+
px: '$4',
60+
}),
61+
row: cssObj({
62+
'.fuel_Text:first-of-type': {
63+
flex: 1,
64+
},
65+
'.small, .fuel_Icon': {
66+
fontSize: '$sm',
67+
color: '$textMuted',
68+
},
69+
}),
70+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Box } from '@fuel-ui/react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
4+
import type { TxStatus, TxType } from '../../types';
5+
import { TX_STATUS, TX_TYPES } from '../../types';
6+
7+
import { TxIcon } from './TxIcon';
8+
9+
const meta: Meta<typeof TxIcon> = {
10+
title: 'Transaction/TxIcon',
11+
component: TxIcon,
12+
};
13+
14+
export default meta;
15+
type Story = StoryObj<typeof TxIcon>;
16+
17+
export const Usage: Story = {
18+
render: () => (
19+
<Box.VStack>
20+
{TX_TYPES.map((type) => (
21+
<Box.HStack key={type}>
22+
{TX_STATUS.map((status) => (
23+
<TxIcon
24+
key={status}
25+
type={type as TxType}
26+
status={status as TxStatus}
27+
/>
28+
))}
29+
</Box.HStack>
30+
))}
31+
</Box.VStack>
32+
),
33+
};
34+
35+
export const Sizes: Story = {
36+
render: () => (
37+
<Box.HStack>
38+
<TxIcon type="contract-call" status="success" size="sm" />
39+
<TxIcon type="contract-call" status="success" size="md" />
40+
<TxIcon type="contract-call" status="success" size="lg" />
41+
</Box.HStack>
42+
),
43+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { LayerIntent } from '@fuel-ui/css';
2+
import { Badge, Icon } from '@fuel-ui/react';
3+
4+
import { type TxStatus, type TxType } from '../../types';
5+
6+
type TxIconProps = {
7+
type: TxType;
8+
status: TxStatus;
9+
size?: 'sm' | 'md' | 'lg';
10+
};
11+
12+
const TX_ICON_MAP: Record<TxType, string> = {
13+
'contract-call': 'Code',
14+
mint: 'Coins',
15+
transfer: 'Transfer',
16+
burn: 'Flame',
17+
};
18+
19+
const TX_INTENT_MAP: Record<TxStatus, LayerIntent> = {
20+
success: 'success',
21+
error: 'error',
22+
pending: 'warning',
23+
idle: 'base',
24+
};
25+
26+
const TX_STATUS_MAP: Record<TxStatus, string> = {
27+
success: 'Success',
28+
error: 'Error',
29+
pending: 'Pending',
30+
idle: 'Idle',
31+
};
32+
33+
export function TxIcon({ type, status, size = 'md' }: TxIconProps) {
34+
const icon = <Icon icon={TX_ICON_MAP[type]} />;
35+
const label = TX_STATUS_MAP[status];
36+
return (
37+
<Badge
38+
variant="ghost"
39+
intent={TX_INTENT_MAP[status]}
40+
aria-label={label}
41+
css={styles.root}
42+
data-size={size}
43+
>
44+
{icon}
45+
</Badge>
46+
);
47+
}
48+
49+
const styles = {
50+
root: {
51+
display: 'inline-flex',
52+
alignItems: 'center',
53+
justifyContent: 'center',
54+
55+
'&[data-size="sm"]': {
56+
width: '$8',
57+
height: '$8',
58+
},
59+
'&[data-size="md"]': {
60+
width: '$10',
61+
height: '$10',
62+
},
63+
'&[data-size="lg"]': {
64+
width: '$12',
65+
height: '$12',
66+
67+
'.fuel_Icon svg': {
68+
width: '24px !important',
69+
height: '24px !important',
70+
},
71+
},
72+
},
73+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Box } from '@fuel-ui/react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
4+
import { TX_TYPES, type TxType } from '../../types';
5+
6+
import { TxTitle } from './TxTitle';
7+
8+
const meta: Meta<typeof TxTitle> = {
9+
title: 'Transaction/TxTitle',
10+
component: TxTitle,
11+
};
12+
13+
export default meta;
14+
type Story = StoryObj<typeof TxTitle>;
15+
16+
export const Usage: Story = {
17+
render: () => (
18+
<TxTitle
19+
type="contract-call"
20+
status="success"
21+
txHash="0x78d13f111bf301324f34f2a7eaffc546d39598d156af38e7c4ef9fe61ea2c46a"
22+
/>
23+
),
24+
};
25+
26+
export const AllTypes: Story = {
27+
render: () => (
28+
<Box.VStack>
29+
{TX_TYPES.map((type) => (
30+
<TxTitle
31+
key={type}
32+
type={type as TxType}
33+
status="idle"
34+
txHash="0x78d13f111bf301324f34f2a7eaffc546d39598d156af38e7c4ef9fe61ea2c46a"
35+
/>
36+
))}
37+
</Box.VStack>
38+
),
39+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { cssObj } from '@fuel-ui/css';
2+
import type { BaseProps } from '@fuel-ui/react';
3+
import { Box, Copyable, Heading } from '@fuel-ui/react';
4+
import { useMemo } from 'react';
5+
import { shortAddress } from '~/systems/Core/utils/address';
6+
7+
import type { TxStatus, TxType } from '../../types';
8+
import { TxIcon } from '../TxIcon/TxIcon';
9+
10+
const TITLE_MAP: Record<TxType, string> = {
11+
'contract-call': 'Contract Call',
12+
transfer: 'Transfer',
13+
mint: 'Mint',
14+
burn: 'Burn',
15+
};
16+
17+
type TxTitleProps = BaseProps<{
18+
status: TxStatus;
19+
type: TxType;
20+
txHash: string;
21+
}>;
22+
23+
export function TxTitle({ status, type, txHash, css, ...props }: TxTitleProps) {
24+
const title = useMemo(() => TITLE_MAP[type], [type]);
25+
return (
26+
<Box.HStack {...props} css={{ ...styles.root, ...css }} gap="$3">
27+
<TxIcon type={type} status={status} />
28+
<Box>
29+
<Heading as="h4">{title}</Heading>
30+
<Copyable value={txHash}>{shortAddress(txHash)}</Copyable>
31+
</Box>
32+
</Box.HStack>
33+
);
34+
}
35+
36+
const styles = {
37+
root: cssObj({
38+
alignItems: 'center',
39+
40+
'.fuel_Heading': {
41+
margin: '$0',
42+
lineHeight: '$tighter',
43+
fontSize: '$md',
44+
},
45+
'.fuel_Copyable': {
46+
fontSize: '$sm',
47+
},
48+
'.fuel_Copyable .fuel_Icon svg': {
49+
width: '14px !important',
50+
height: '14px !important',
51+
},
52+
}),
53+
};

0 commit comments

Comments
 (0)