From 3e4f38e3efa48d945223f0de92c0b5cf07ebb117 Mon Sep 17 00:00:00 2001 From: secondl1ght Date: Thu, 3 Oct 2024 17:27:14 -0600 Subject: [PATCH 1/3] feat: detailed and recent transactions --- 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 6edaba88..86b89170 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 0d3b574d..df837c80 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": "Recent Transactions", "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": "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": "Sin resultados.", - "pending": "Procesando" + "paid": "Paid", + "pending": "Procesando", + "received": "Received", + "sent": "Sent", + "status": "Status", + "time": "Time", + "unblinded": "Copy Unblinded URL" }, "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 b43500ec..e593802a 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..184fb587 --- /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) => ( )) ) : ( From d9857a191ef66dcdfd566d7c23559fc8b4dc53f4 Mon Sep 17 00:00:00 2001 From: secondl1ght Date: Thu, 3 Oct 2024 18:39:36 -0600 Subject: [PATCH 2/3] style: hover states on tx buttons --- src/views/transactions/Transaction.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/transactions/Transaction.tsx b/src/views/transactions/Transaction.tsx index 184fb587..f37dd119 100644 --- a/src/views/transactions/Transaction.tsx +++ b/src/views/transactions/Transaction.tsx @@ -29,9 +29,9 @@ export const Transaction: FC<{ return ( -
+
-
+
{balanceNum < 0 ? ( ) : ( From 147d945aa05c1ba16a7cc50c41248c393113447d Mon Sep 17 00:00:00 2001 From: Anthony Potdevin Date: Sun, 6 Oct 2024 20:46:23 -0600 Subject: [PATCH 3/3] chore: lang strings --- messages/es.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/messages/es.json b/messages/es.json index df837c80..e07dc049 100644 --- a/messages/es.json +++ b/messages/es.json @@ -9,7 +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": "Recent Transactions", + "transactions": "Transacciones Recientes", "warning": "Para enviar USD tienes que tener Bitcoin en tu billetera.", "warning-title": "Agrega Bitcoin!", "what-asset": "Qué es {asset}?" @@ -28,22 +28,22 @@ "Transactions": { "ago": "Hace", "all": "Todas", - "amount": "Amount", - "asset": "Asset", - "blinded": "Copy Blinded URL", - "copy-id": "Copy ID", - "copy-tx": "Copy Transaction ID", - "date": "Date", - "details": "Transaction Details", - "fees": "Fees", + "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.", - "paid": "Paid", + "paid": "Pagado", "pending": "Procesando", - "received": "Received", - "sent": "Sent", - "status": "Status", - "time": "Time", - "unblinded": "Copy Unblinded URL" + "received": "Recivido", + "sent": "Envíado", + "status": "Estado", + "time": "Tiempo", + "unblinded": "Copiar URL Público" }, "Vault": { "lock": "Bloquear",