Skip to content

Commit

Permalink
Transaction table and transactions details (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
avkos authored Jan 15, 2025
1 parent c04cdc5 commit 5cd0571
Show file tree
Hide file tree
Showing 29 changed files with 643 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ export interface ComplianceEngineTextProps {

export function ComplianceEngineText({ result }: ComplianceEngineTextProps) {
return result ? (
<Badge variant="accent" className="font-normal text-green-600 dark:text-green-500">
<Badge
variant="accent"
className="min-h-6 font-normal text-green-600 dark:text-green-500"
>
Approved by Circle Compliance Engine ✓
</Badge>
) : (
<Badge variant="accent" className="font-normal text-red-500 dark:text-red-400">
<Badge
variant="accent"
className="min-h-6 font-normal text-red-500 dark:text-red-400"
>
Denied by Circle Compliance Engine ✘
</Badge>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Token } from '~/lib/types';

import { TokenItem } from './TokenItem';

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

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
token: {
blockchain: 'ARB-SEPOLIA',
createDate: '2024-08-12T21:58:31Z',
decimals: 18,
id: '9ad91eb5-e152-5d81-b60e-151d5fd2b3d3',
isNative: true,
name: 'Arbitrum Ethereum-Sepolia',
symbol: 'ETH-SEPOLIA',
updateDate: '2024-08-12T21:58:31Z',
} as Token,
},
};
23 changes: 23 additions & 0 deletions packages/circle-demo-webapp/app/components/TokenItem/TokenItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TokenIcon } from '@web3icons/react';

import { Token } from '~/lib/types';

export interface TokenItemProps {
/** The balance details */
token: Token;
}

/** A token balance for an on-chain account */
export function TokenItem({ token }: TokenItemProps) {
return (
<div className="flex items-center space-x-2">
<TokenIcon
symbol={token.symbol.split('-')[0]}
size={24}
variant="branded"
className="flex-shrink-0"
/>
<div className="text-sm text-muted-foreground">{token.symbol}</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TokenItem';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';

import { TransactionState } from '~/lib/constants';

import { TransactionStateText } from './TransactionStateText';

const meta = {
title: 'TransactionStateText',
component: TransactionStateText,
tags: ['autodocs'],
} satisfies Meta<typeof TransactionStateText>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
state: TransactionState.Complete,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Badge } from '~/components/ui/badge';
import { TransactionState } from '~/lib/constants';

export interface TransactionStateTextProps {
state: (typeof TransactionState)[keyof typeof TransactionState];
}

const greenStates = [TransactionState.Complete, TransactionState.Confirmed];
const yellowStates = [
TransactionState.Sent,
TransactionState.Initiated,
TransactionState.Queued,
TransactionState.PendingRiskScreening,
];

const capitalize = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();

export function TransactionStateText({ state }: TransactionStateTextProps) {
return greenStates.includes(state) ? (
<Badge variant="accent" className="font-normal text-green-600 dark:text-green-500">
{capitalize(state)}
</Badge>
) : yellowStates.includes(state) ? (
<Badge variant="accent" className="font-normal text-yellow-500 dark:text-yellow-400">
{capitalize(state)}
</Badge>
) : (
<Badge variant="accent" className="font-normal text-red-500 dark:text-red-400">
{capitalize(state)}
</Badge>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TransactionStateText';
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ export function TransactionTableHead() {
return (
<thead>
<tr className="text-foreground text-left text-sm">
<th className="py-2">Event</th>
<th className="px-4 py-2">From</th>
<th className="px-4 py-2">To</th>
<th className="px-4 py-2">Status</th>
<th className="px-4 py-2">Token Name</th>
<th className="px-4 py-2 text-right">Amount</th>
<th className="py-2 text-right">Date</th>
<th className="px-4 py-2 text-right">Date</th>
</tr>
</thead>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
import { Badge } from '~/components/ui/badge';
import { TokenItem } from '~/components/TokenItem';
import { TransactionStateText } from '~/components/TransactionStatusText';
import { TransactionType } from '~/lib/constants';
import { formatDate, shortenAddress } from '~/lib/format';
import { Transaction } from '~/lib/types';
import { TransactionWithToken } from '~/lib/types';

export interface TransactionTableRowProps {
transaction: Transaction;
transaction: TransactionWithToken;
}

export function TransactionTableRow({ transaction }: TransactionTableRowProps) {
const isInbound = transaction.transactionType === TransactionType.Inbound;

return (
<tr className="text-sm text-muted-foreground">
<td className="py-2">
<Badge variant="outline">{transaction.operation}</Badge>
</td>

<td className="px-4 py-2" title={transaction.sourceAddress}>
{shortenAddress(transaction.sourceAddress)}
</td>

<td className="px-4 py-2" title={transaction.destinationAddress}>
{shortenAddress(transaction.destinationAddress)}
</td>

<td className="px-4 py-2" title={transaction.state}>
<TransactionStateText state={transaction.state} />
</td>
<td className="px-4 py-2" title={transaction.tokenId}>
{transaction?.token ? <TokenItem token={transaction.token} /> : '-'}
</td>
<td
className={`px-4 py-2 text-right font-medium ${
isInbound ? 'text-green-600' : 'text-destructive'
}`}
>
{isInbound ? '+' : '-'} {transaction.amounts?.[0] ?? '0.00'}
</td>

<td className="py-2 text-right">
<td className="px-4 py-2 text-right">
{formatDate(transaction.firstConfirmDate ?? transaction.createDate)}
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Input } from '~/components/ui/input';
import { Textarea } from '~/components/ui/textarea';
import { WalletDetails } from '~/components/WalletDetails';
import { FeeLevel } from '~/lib/constants';
import { CircleError } from '~/lib/responses';
import { CircleError, ErrorResponse } from '~/lib/responses';
import { Transaction, Wallet, WalletTokenBalance } from '~/lib/types';
import { isAddress, isNumber } from '~/lib/utils';

Expand Down Expand Up @@ -91,12 +91,11 @@ export function WalletSend({
},
},
} as CreateTransactionInput);
if (res as CircleError) {
setRequestError((res as CircleError).message);
if ((res as unknown as ErrorResponse)?.error) {
setRequestError((res as unknown as ErrorResponse).error);
return;
}
const tx = res as Transaction;

setTransactionData({ state: tx.state } as Transaction);
if (tx.id) {
const interval = setInterval(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/circle-demo-webapp/app/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from 'react';

import { cn } from '~/lib/utils';

const buttonVariants = cva(
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
Expand Down
31 changes: 31 additions & 0 deletions packages/circle-demo-webapp/app/components/ui/inputWithIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Search } from 'lucide-react';
import * as React from 'react';

import { cn } from '~/lib/utils';

interface InputProps extends React.ComponentProps<'input'> {
className?: string;
type?: string;
}

const InputWithIcon = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<div className="relative">
<Search className="absolute mt-2 ml-[9px] w-[17px]" />
<input
type={type}
className={cn(
'border border-input py-2 justify-between h-10 flex w-full px-8 rounded-md bg-background text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
)}
ref={ref}
{...props}
/>
</div>
);
},
);
InputWithIcon.displayName = 'InputWithIcon';

export { InputWithIcon };
112 changes: 112 additions & 0 deletions packages/circle-demo-webapp/app/components/ui/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import * as React from 'react';

import { ButtonProps, buttonVariants } from '~/components/ui/button';
import { cn } from '~/lib/utils';

const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';

const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn('flex flex-row items-center gap-1', className)}
{...props}
/>
),
);
PaginationContent.displayName = 'PaginationContent';

const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
),
);
PaginationItem.displayName = 'PaginationItem';

type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;

const PaginationLink = ({
className,
isActive,
size = 'icon',
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
className,
)}
{...props}
/>
);
PaginationLink.displayName = 'PaginationLink';

const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';

const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';

const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';

export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};
Loading

0 comments on commit 5cd0571

Please sign in to comment.