From 79fac2fbf1db58ca8c085c25d0c59e121b20844f Mon Sep 17 00:00:00 2001 From: secondl1ght <85003930+secondl1ght@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:50:36 -0600 Subject: [PATCH] feat: detailed and recent transactions (#87) --- messages/en.json | 17 +- messages/es.json | 19 +- .../(app)/(layout)/transactions/[id]/page.tsx | 5 + src/utils/routes.ts | 5 +- src/views/dashboard/Dashboard.tsx | 2 + src/views/dashboard/RecentTransactions.tsx | 110 +++++++ src/views/transactions/Transaction.tsx | 79 +++++ src/views/transactions/TransactionDetail.tsx | 272 ++++++++++++++++++ src/views/transactions/Transactions.tsx | 105 ++----- src/views/transactions/TransactionsTable.tsx | 3 +- 10 files changed, 534 insertions(+), 83 deletions(-) create mode 100644 src/app/(app)/(layout)/transactions/[id]/page.tsx create mode 100644 src/views/dashboard/RecentTransactions.tsx create mode 100644 src/views/transactions/Transaction.tsx create mode 100644 src/views/transactions/TransactionDetail.tsx diff --git a/messages/en.json b/messages/en.json index 936247ff..59d4d1b4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -9,6 +9,7 @@ "recent": "Recent Contacts", "sell": "Sell", "tether-explainer": "Tether (USDT) is a stablecoin, a type of cryptocurrency designed to maintain a stable value relative to a fiat currency, in this case, the United States dollar (USD). It is issued by Tether Limited, a company founded in 2014, and is pegged to the value of the US dollar.", + "transactions": "Recent Transactions", "warning": "In order to send USD you need to add Bitcoin to your wallet.", "warning-title": "Add Bitcoin!", "what-asset": "What is {asset}?" @@ -27,8 +28,22 @@ "Transactions": { "ago": "ago", "all": "All", + "amount": "Amount", + "asset": "Asset", + "blinded": "Copy Blinded URL", + "copy-id": "Copy ID", + "copy-tx": "Copy Transaction ID", + "date": "Date", + "details": "Transaction Details", + "fees": "Fees", "no-results": "No results.", - "pending": "Pending" + "paid": "Paid", + "pending": "Pending", + "received": "Received", + "sent": "Sent", + "status": "Status", + "time": "Time", + "unblinded": "Copy Unblinded URL" }, "Vault": { "lock": "Lock", diff --git a/messages/es.json b/messages/es.json index fcfaf618..5e3937f2 100644 --- a/messages/es.json +++ b/messages/es.json @@ -9,6 +9,7 @@ "recent": "Contactos Recientes", "sell": "Vender", "tether-explainer": "Tether (USDT) es una stablecoin, un tipo de criptomoneda diseñada para mantener un valor estable en relación con una moneda fiduciaria, en este caso, el dólar estadounidense (USD). Es emitida por Tether Limited, una compañía fundada en 2014, y está vinculada al valor del dólar estadounidense.", + "transactions": "Transacciones Recientes", "warning": "Para enviar USD tienes que tener Bitcoin en tu billetera.", "warning-title": "Agrega Bitcoin!", "what-asset": "Qué es {asset}?" @@ -25,10 +26,24 @@ "unlock": "Desbloquear para Desencriptar" }, "Transactions": { - "ago": "ago", + "ago": "Hace", "all": "Todas", + "amount": "Cantidad", + "asset": "Activo", + "blinded": "Copiar URL Privado", + "copy-id": "Copiar ID", + "copy-tx": "Copiar ID de la Transacción", + "date": "Fecha", + "details": "Detalles de la Transacción", + "fees": "Comisiónes", "no-results": "Sin resultados.", - "pending": "Procesando" + "paid": "Pagado", + "pending": "Procesando", + "received": "Recivido", + "sent": "Envíado", + "status": "Estado", + "time": "Tiempo", + "unblinded": "Copiar URL Público" }, "Vault": { "lock": "Bloquear", diff --git a/src/app/(app)/(layout)/transactions/[id]/page.tsx b/src/app/(app)/(layout)/transactions/[id]/page.tsx new file mode 100644 index 00000000..f33129b3 --- /dev/null +++ b/src/app/(app)/(layout)/transactions/[id]/page.tsx @@ -0,0 +1,5 @@ +import { TransactionDetail } from '@/views/transactions/TransactionDetail'; + +export default function Page({ params }: { params: { id: string } }) { + return ; +} diff --git a/src/utils/routes.ts b/src/utils/routes.ts index 58fcc265..376ffd5c 100644 --- a/src/utils/routes.ts +++ b/src/utils/routes.ts @@ -9,7 +9,10 @@ export const ROUTES = { home: '/settings', twofa: '/settings/2fa', }, - transactions: { home: '/transactions' }, + transactions: { + home: '/transactions', + tx: (id: string) => `/transactions/${id}`, + }, swaps: { home: '/swaps', }, diff --git a/src/views/dashboard/Dashboard.tsx b/src/views/dashboard/Dashboard.tsx index 1b0b50cf..5284f859 100644 --- a/src/views/dashboard/Dashboard.tsx +++ b/src/views/dashboard/Dashboard.tsx @@ -17,6 +17,7 @@ import { ROUTES } from '@/utils/routes'; import { WalletInfo } from '../wallet/WalletInfo'; import { BancoCode } from './BancoCode'; import { RecentContacts } from './RecentContacts'; +import { RecentTransactions } from './RecentTransactions'; export type DashboardView = 'default' | 'assets' | 'asset'; @@ -108,6 +109,7 @@ export const Dashboard = () => { + ) : ( diff --git a/src/views/dashboard/RecentTransactions.tsx b/src/views/dashboard/RecentTransactions.tsx new file mode 100644 index 00000000..28f35dbf --- /dev/null +++ b/src/views/dashboard/RecentTransactions.tsx @@ -0,0 +1,110 @@ +import { sortBy } from 'lodash'; +import Link from 'next/link'; +import { useTranslations } from 'next-intl'; +import { FC, useMemo } from 'react'; + +import { Skeleton } from '@/components/ui/skeleton'; +import { useToast } from '@/components/ui/use-toast'; +import { useGetWalletQuery } from '@/graphql/queries/__generated__/wallet.generated'; +import { handleApolloError } from '@/utils/error'; +import { cryptoToUsd } from '@/utils/fiat'; +import { ROUTES } from '@/utils/routes'; + +import { Transaction } from '../transactions/Transaction'; +import { TransactionEntry } from '../transactions/Transactions'; + +export const RecentTransactions: FC<{ id: string }> = ({ id }) => { + const t = useTranslations('App'); + const { toast } = useToast(); + + const { data, loading, error } = useGetWalletQuery({ + variables: { id }, + onError: err => { + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error getting transactions.', + description: messages.join(', '), + }); + }, + }); + + const transactions = useMemo(() => { + if (loading || error) return []; + if (!data?.wallets.find_one.accounts.length) return []; + + const { accounts } = data.wallets.find_one; + + const transactions: TransactionEntry[] = []; + + accounts.forEach(a => { + if (!a.liquid) return; + + a.liquid.transactions.forEach(t => { + transactions.push({ + id: t.id, + balance: t.balance, + formatted_balance: cryptoToUsd( + t.balance, + t.asset_info.precision, + t.asset_info.ticker, + t.fiat_info.fiat_to_btc + ), + date: t.date, + ticker: t.asset_info.ticker, + precision: t.asset_info.precision, + }); + }); + }); + + const sorted = sortBy(transactions, t => + t.date ? new Date(t.date) : new Date() + ).reverse(); + + return sorted.slice(0, 5); + }, [data, loading, error]); + + return ( +
+
+

{t('Dashboard.transactions')}

+ + + {t('view-all')} + +
+ +
+ {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + )) + ) : ( + <> + {transactions.length ? ( + transactions.map(t => ( + + )) + ) : ( +

+ {t('Wallet.Transactions.no-results')} +

+ )} + + )} +
+
+ ); +}; diff --git a/src/views/transactions/Transaction.tsx b/src/views/transactions/Transaction.tsx new file mode 100644 index 00000000..f37dd119 --- /dev/null +++ b/src/views/transactions/Transaction.tsx @@ -0,0 +1,79 @@ +import { format, formatDistanceToNowStrict } from 'date-fns'; +import { es } from 'date-fns/locale'; +import { ArrowDown, ArrowUp } from 'lucide-react'; +import Link from 'next/link'; +import { useLocale, useTranslations } from 'next-intl'; +import { FC } from 'react'; + +import { cn } from '@/utils/cn'; +import { numberWithPrecisionAndDecimals } from '@/utils/numbers'; +import { ROUTES } from '@/utils/routes'; + +export const Transaction: FC<{ + id: string; + balance: string; + precision: number; + date: string | undefined | null; + formatted_balance: string; + ticker: string; +}> = ({ id, balance, precision, date, formatted_balance, ticker }) => { + const t = useTranslations(); + const locale = useLocale(); + + const balanceNum = Number(balance); + + const formatted = numberWithPrecisionAndDecimals( + parseFloat(balance), + precision + ); + + return ( + +
+
+
+ {balanceNum < 0 ? ( + + ) : ( + + )} +
+ + {date ? ( +
+

+ {locale === 'es' + ? t('App.Wallet.Transactions.ago') + + ' ' + + formatDistanceToNowStrict(date, { + locale: es, + }) + : formatDistanceToNowStrict(date) + + ' ' + + t('App.Wallet.Transactions.ago')} +

+ +

+ {format(date, 'MMM dd, yyyy')} +

+
+ ) : ( +

{t('App.Wallet.Transactions.pending')}

+ )} +
+ +
+

0 && 'text-green-400')}> + {formatted_balance.includes('-') + ? '-' + formatted_balance.replaceAll('-', '') + : '+' + formatted_balance} +

+ +

+ {formatted.includes('-') ? formatted : '+' + formatted} {ticker} +

+
+
+ + ); +}; diff --git a/src/views/transactions/TransactionDetail.tsx b/src/views/transactions/TransactionDetail.tsx new file mode 100644 index 00000000..18047ed7 --- /dev/null +++ b/src/views/transactions/TransactionDetail.tsx @@ -0,0 +1,272 @@ +'use client'; + +import { format } from 'date-fns'; +import { + ArrowDown, + ArrowLeft, + ArrowUp, + ArrowUpDown, + Copy, + Ellipsis, + Link as LinkIcon, +} from 'lucide-react'; +import Link from 'next/link'; +import { useTranslations } from 'next-intl'; +import { FC, useMemo, useState } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useToast } from '@/components/ui/use-toast'; +import { useGetWalletQuery } from '@/graphql/queries/__generated__/wallet.generated'; +import { cn } from '@/utils/cn'; +import { LOCALSTORAGE_KEYS } from '@/utils/constants'; +import { handleApolloError } from '@/utils/error'; +import { cryptoToUsd } from '@/utils/fiat'; +import { numberWithPrecisionAndDecimals } from '@/utils/numbers'; +import { ROUTES } from '@/utils/routes'; + +export const TransactionDetail: FC<{ id: string }> = ({ id }) => { + const t = useTranslations('App.Wallet.Transactions'); + const { toast } = useToast(); + + const [value] = useLocalStorage(LOCALSTORAGE_KEYS.currentWalletId, ''); + + const [assetFirst, setAssetFirst] = useState(false); + + const { data, loading, error } = useGetWalletQuery({ + variables: { id: value }, + skip: !value, + onCompleted: data => { + const tx = data?.wallets.find_one.accounts + .find(a => a.liquid) + ?.liquid?.transactions.find(t => t.id === id); + + if (!tx) { + toast({ + variant: 'destructive', + title: 'Transaction not found.', + }); + } + }, + onError: err => { + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error getting transaction.', + description: messages.join(', '), + }); + }, + }); + + const transaction = useMemo( + () => + data?.wallets.find_one.accounts + .find(a => a.liquid) + ?.liquid?.transactions.find(t => t.id === id), + [data?.wallets.find_one.accounts, id] + ); + + if (loading || error || !transaction) + return ( +
+
+ + + + +

{t('details')}

+ + +
+ + {loading ? ( +
+ + +
+ ) : null} +
+ ); + + const balance = Number(transaction.balance); + + const fiatBalance = cryptoToUsd( + transaction.balance, + transaction.asset_info.precision, + transaction.asset_info.ticker, + transaction.fiat_info.fiat_to_btc + ); + + const prefixFiatBalance = fiatBalance.includes('-') + ? '-' + fiatBalance.replaceAll('-', '') + : '+' + fiatBalance; + + const assetBalance = numberWithPrecisionAndDecimals( + parseFloat(transaction.balance), + transaction.asset_info.precision + ); + + const prefixAssetBalance = + (assetBalance.includes('-') ? assetBalance : '+' + assetBalance) + + ' ' + + transaction.asset_info.ticker; + + return ( +
+
+ + + + +

{t('details')}

+ + + + + + + + navigator.clipboard.writeText(transaction.id)} + > + +

{t('copy-id')}

+
+ + navigator.clipboard.writeText(transaction.tx_id)} + > + +

{t('copy-tx')}

+
+ + + navigator.clipboard.writeText(transaction.blinded_url) + } + > + +

{t('blinded')}

+
+ + + navigator.clipboard.writeText(transaction.unblinded_url) + } + > + +

{t('unblinded')}

+
+
+
+
+ +
+
+
+ {balance < 0 ? ( + + ) : ( + + )} +
+ +

+ {balance < 0 ? t('sent') : t('received')} +

+ +

+ {assetFirst ? prefixAssetBalance : prefixFiatBalance} +

+ +
+

+ {assetFirst ? prefixFiatBalance : prefixAssetBalance} +

+ + +
+
+ +
+
+

+ {t('date')} +

+ +

+ {transaction.date + ? format(transaction.date, 'MMM dd, yyyy') + : '-'} +

+
+ +
+

+ {t('time')} +

+ +

{transaction.date ? format(transaction.date, 'HH:mm') : '-'}

+
+ +
+

+ {t('status')} +

+ +

+ {transaction.date ? t('paid') : t('pending')} +

+
+ +
+

+ {t('amount')} +

+ +

{fiatBalance.replaceAll('-', '')}

+
+ +
+

+ {t('asset')} +

+ +

{transaction.asset_info.name}

+
+ +
+

+ {t('fees')} +

+ +

{Number(transaction.fee).toLocaleString('en-US')} sats

+
+
+
+
+ ); +}; diff --git a/src/views/transactions/Transactions.tsx b/src/views/transactions/Transactions.tsx index 51e1b67a..0b403991 100644 --- a/src/views/transactions/Transactions.tsx +++ b/src/views/transactions/Transactions.tsx @@ -1,38 +1,52 @@ 'use client'; import { ColumnDef } from '@tanstack/react-table'; -import { format, formatDistanceToNowStrict } from 'date-fns'; -import { es } from 'date-fns/locale'; import { sortBy } from 'lodash'; -import { ArrowDown, ArrowUp } from 'lucide-react'; -import { useLocale, useTranslations } from 'next-intl'; +import { useTranslations } from 'next-intl'; import { useMemo } from 'react'; import { useLocalStorage } from 'usehooks-ts'; import { RefreshButton } from '@/components/button/RefreshButton'; import { useToast } from '@/components/ui/use-toast'; import { useGetWalletQuery } from '@/graphql/queries/__generated__/wallet.generated'; -import { cn } from '@/utils/cn'; import { LOCALSTORAGE_KEYS } from '@/utils/constants'; import { handleApolloError } from '@/utils/error'; import { cryptoToUsd } from '@/utils/fiat'; -import { numberWithPrecisionAndDecimals } from '@/utils/numbers'; +import { Transaction } from './Transaction'; import { TransactionsTable } from './TransactionsTable'; -type TransactionEntry = { - tx_id: string; +export type TransactionEntry = { + id: string; balance: string; formatted_balance: string; date: string | undefined | null; ticker: string; precision: number; - name: string; + name?: string; }; +const columns: ColumnDef[] = [ + { + id: 'transaction', + accessorKey: 'name', + cell: ({ row }) => { + return ( + + ); + }, + }, +]; + export const Transactions = () => { - const t = useTranslations(); - const locale = useLocale(); + const t = useTranslations('Index'); const { toast } = useToast(); @@ -65,7 +79,7 @@ export const Transactions = () => { a.liquid.transactions.forEach(t => { transactions.push({ - tx_id: t.tx_id, + id: t.id, balance: t.balance, formatted_balance: cryptoToUsd( t.balance, @@ -88,75 +102,10 @@ export const Transactions = () => { return sorted; }, [data, loading, error]); - const columns: ColumnDef[] = useMemo( - () => [ - { - id: 'transaction', - accessorKey: 'name', - cell: ({ row }) => { - const balance = Number(row.original.balance); - - const formatted = numberWithPrecisionAndDecimals( - parseFloat(row.original.balance), - row.original.precision - ); - - return ( -
-
-
- {balance < 0 ? ( - - ) : ( - - )} -
- - {row.original.date ? ( -
-

- {formatDistanceToNowStrict(row.original.date, { - locale: locale === 'es' ? es : undefined, - }) + - ' ' + - t('App.Wallet.Transactions.ago')} -

- -

- {format(row.original.date, 'MMM dd, yyyy')} -

-
- ) : ( -

{t('App.Wallet.Transactions.pending')}

- )} -
- -
-

0 && 'text-green-400')} - > - {row.original.formatted_balance.includes('-') - ? '-' + row.original.formatted_balance.replaceAll('-', '') - : '+' + row.original.formatted_balance} -

- -

- {formatted.includes('-') ? formatted : '+' + formatted}{' '} - {row.original.ticker} -

-
-
- ); - }, - }, - ], - [locale, t] - ); - return (
-

{t('Index.transactions')}

+

{t('transactions')}

diff --git a/src/views/transactions/TransactionsTable.tsx b/src/views/transactions/TransactionsTable.tsx index 8647d70f..1507d40d 100644 --- a/src/views/transactions/TransactionsTable.tsx +++ b/src/views/transactions/TransactionsTable.tsx @@ -47,6 +47,7 @@ export const TransactionsTable = ({ state: { columnFilters, }, + initialState: { pagination: { pageSize: 5 } }, }); return ( @@ -78,7 +79,7 @@ export const TransactionsTable = ({
{loading ? ( - Array.from({ length: 10 }).map((_, i) => ( + Array.from({ length: 5 }).map((_, i) => ( )) ) : (