diff --git a/.env.example b/.env.example index 9dc7eb7..a1d86ab 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ +VITE_ENABLE_PROXY= VITE_QLI_API_URL= VITE_ARCHIVER_API_URL= diff --git a/README.md b/README.md index 0f3cc75..e4a693b 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,7 @@ cd ### Step 2: Configure Environment Variables -Before running the project, you must configure environment-specific variables for development and -production environments. +Before running the project, you must configure environment-specific variables for development environment. - **Development Environment**: @@ -30,25 +29,11 @@ production environments. Open this file and add the development API URL: ``` + VITE_ENABLE_PROXY=true VITE_QLI_API_URL=/dev-proxy-qli-api VITE_ARCHIVER_API_URL=/dev-proxy-archiver-api ``` -- **Production Environment**: - - Copy the `.env.example` file, renaming it to `.env.production.local`: - - ``` - cp .env.production.local.example .env.production.local - ``` - - Then, set the production API URL: - - ``` - VITE_QLI_API_URL=https://api.qubic.li - VITE_ARCHIVER_API_URL=https://rpc.qubic.org/v1 - ``` - Ensure these files are not committed to the repository to protect sensitive information. ### Step 3: Install Dependencies diff --git a/dev-proxy.config.ts b/dev-proxy.config.ts index 988b1f4..4a79f8c 100644 --- a/dev-proxy.config.ts +++ b/dev-proxy.config.ts @@ -1,69 +1,51 @@ import type { HttpProxy, ProxyOptions } from 'vite' -export const qliApiProxy: ProxyOptions = { - target: 'https://api.qubic.li', - changeOrigin: true, - rewrite: (path: string) => path.replace(/^\/dev-proxy-qli-api/, ''), - configure: (proxy: HttpProxy.Server, options: ProxyOptions) => { - proxy.on('proxyReq', (proxyReq, req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE') - res.setHeader( - 'Access-Control-Allow-Headers', - req.headers['access-control-request-headers'] || '' - ) - - if (req.method === 'OPTIONS') { - res.writeHead(200) - res.end() - return - } - - // eslint-disable-next-line no-console - console.log(`[QLI-API-DEV-PROXY] - API CALL - [${req.method}] ${options.target}${req.url}`) - proxyReq.setHeader('Authorization', req.headers.authorization || '') - }) - - proxy.on('error', (err, _req, res) => { - // eslint-disable-next-line no-console - console.error(`Proxy error: ${err.message}`) - res.writeHead(500, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ error: 'Proxy error', details: err.message })) - }) +export const createProxyConfig = ( + target: string, + rewritePath: string, + label = 'PROXY' +): ProxyOptions => { + return { + target, + changeOrigin: true, + rewrite: (path: string) => path.replace(rewritePath, ''), + configure: (proxy: HttpProxy.Server, options: ProxyOptions) => { + proxy.on('proxyReq', (proxyReq, req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE') + res.setHeader( + 'Access-Control-Allow-Headers', + req.headers['access-control-request-headers'] || '' + ) + + if (req.method === 'OPTIONS') { + res.writeHead(200) + res.end() + return + } + + // eslint-disable-next-line no-console + console.log(`[${label}] - API CALL - [${req.method}] ${options.target}${req.url}`) + proxyReq.setHeader('Authorization', req.headers.authorization || '') + }) + + proxy.on('error', (err, _req, res) => { + // eslint-disable-next-line no-console + console.error(`Proxy error: ${err.message}`) + res.writeHead(500, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Proxy error', details: err.message })) + }) + } } } -export const archiverApiProxy: ProxyOptions = { - target: 'https://rpc.qubic.org/v1', - changeOrigin: true, - rewrite: (path: string) => path.replace(/^\/dev-proxy-archiver-api/, ''), - configure: (proxy: HttpProxy.Server, options: ProxyOptions) => { - proxy.on('proxyReq', (proxyReq, req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE') - res.setHeader( - 'Access-Control-Allow-Headers', - req.headers['access-control-request-headers'] || '' - ) - - if (req.method === 'OPTIONS') { - res.writeHead(200) - res.end() - return - } - - // eslint-disable-next-line no-console - console.log( - `[ARCHIVER-API-DEV-PROXY] - API CALL - [${req.method}] ${options.target}${req.url}` - ) - proxyReq.setHeader('Authorization', req.headers.authorization || '') - }) - - proxy.on('error', (err, _req, res) => { - // eslint-disable-next-line no-console - console.error(`Proxy error: ${err.message}`) - res.writeHead(500, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ error: 'Proxy error', details: err.message })) - }) - } -} +export const qliApiProxy = createProxyConfig( + 'https://api.qubic.li', + '/dev-proxy-qli-api', + 'QLI-API-DEV-PROXY' +) +export const archiverApiProxy = createProxyConfig( + 'https://rpc.qubic.org', + '/dev-proxy-archiver-api', + 'ARCHIVER-API-DEV-PROXY' +) diff --git a/public/locales/ar/network-page.json b/public/locales/ar/network-page.json index bca729d..75b5a53 100644 --- a/public/locales/ar/network-page.json +++ b/public/locales/ar/network-page.json @@ -53,5 +53,6 @@ "rank": "الترتيب", "addressID": "معرّف العنوان", "richListLoadFailed": "خطأ: فشل تحميل قائمة الأثرياء. يرجى تحديث الصفحة أو المحاولة مرة أخرى لاحقًا.", - "richListWarning": "يتم تحديث بيانات قائمة الأثرياء في بداية كل فترة" + "richListWarning": "يتم تحديث بيانات قائمة الأثرياء في بداية كل فترة", + "timestamp": "الطابع الزمني" } diff --git a/public/locales/de/network-page.json b/public/locales/de/network-page.json index 4456ada..60e3328 100644 --- a/public/locales/de/network-page.json +++ b/public/locales/de/network-page.json @@ -53,5 +53,6 @@ "rank": "Rang", "addressID": "Adress-ID", "richListLoadFailed": "Fehler: Reichenliste konnte nicht geladen werden. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.", - "richListWarning": "Die Daten der Reichenliste werden zu Beginn jeder Epoche aktualisiert" + "richListWarning": "Die Daten der Reichenliste werden zu Beginn jeder Epoche aktualisiert", + "timestamp": "Zeitstempel" } diff --git a/public/locales/en/network-page.json b/public/locales/en/network-page.json index 4fa14ed..01e43d9 100644 --- a/public/locales/en/network-page.json +++ b/public/locales/en/network-page.json @@ -53,5 +53,6 @@ "rank": "Rank", "addressID": "Address ID", "richListLoadFailed": "Error: Failed to load rich list. Please refresh the page or try again later.", - "richListWarning": "Rich list data is updated at the beginning of each epoch" + "richListWarning": "Rich list data is updated at the beginning of each epoch", + "timestamp": "Timestamp" } diff --git a/public/locales/es/network-page.json b/public/locales/es/network-page.json index d7dcf4b..f3118cd 100644 --- a/public/locales/es/network-page.json +++ b/public/locales/es/network-page.json @@ -53,5 +53,6 @@ "rank": "Rango", "addressID": "ID de Dirección", "richListLoadFailed": "Error: No se pudo cargar la lista de ricos. Por favor, actualice la página o intente nuevamente más tarde.", - "richListWarning": "Los datos de la lista de ricos se actualizan al comienzo de cada época" + "richListWarning": "Los datos de la lista de ricos se actualizan al comienzo de cada época", + "timestamp": "Fecha" } diff --git a/public/locales/fr/network-page.json b/public/locales/fr/network-page.json index 6a12071..6d4fe76 100644 --- a/public/locales/fr/network-page.json +++ b/public/locales/fr/network-page.json @@ -53,5 +53,6 @@ "rank": "Rang", "addressID": "ID d'adresse", "richListLoadFailed": "Erreur : Impossible de charger la liste des riches. Veuillez actualiser la page ou réessayer plus tard.", - "richListWarning": "Les données de la liste des riches sont mises à jour au début de chaque époque" + "richListWarning": "Les données de la liste des riches sont mises à jour au début de chaque époque", + "timestamp": "Horodatage" } diff --git a/public/locales/ja/network-page.json b/public/locales/ja/network-page.json index 1cb3dd9..dc1a32f 100644 --- a/public/locales/ja/network-page.json +++ b/public/locales/ja/network-page.json @@ -53,5 +53,6 @@ "rank": "ランク", "addressID": "アドレスID", "richListLoadFailed": "エラー:リッチリストの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。", - "richListWarning": "リッチリストのデータは各エポックの開始時に更新されます" + "richListWarning": "リッチリストのデータは各エポックの開始時に更新されます", + "timestamp": "タイムスタンプ" } diff --git a/public/locales/nl/network-page.json b/public/locales/nl/network-page.json index 6e7afca..5e088cd 100644 --- a/public/locales/nl/network-page.json +++ b/public/locales/nl/network-page.json @@ -53,5 +53,6 @@ "rank": "Rang", "addressID": "Adres-ID", "richListLoadFailed": "Fout: Het laden van de rijkelijst is mislukt. Ververs de pagina of probeer het later opnieuw.", - "richListWarning": "Rijkelijstgegevens worden aan het begin van elke epoche bijgewerkt" + "richListWarning": "Rijkelijstgegevens worden aan het begin van elke epoche bijgewerkt", + "timestamp": "Tijdstempel" } diff --git a/public/locales/pt/network-page.json b/public/locales/pt/network-page.json index 12aa391..d6814c7 100644 --- a/public/locales/pt/network-page.json +++ b/public/locales/pt/network-page.json @@ -53,5 +53,6 @@ "rank": "Classificação", "addressID": "ID do Endereço", "richListLoadFailed": "Erro: Falha ao carregar a lista de ricos. Por favor, atualize a página ou tente novamente mais tarde.", - "richListWarning": "Os dados da lista de ricos são atualizados no início de cada época" + "richListWarning": "Os dados da lista de ricos são atualizados no início de cada época", + "timestamp": "Carimbo de tempo" } diff --git a/public/locales/ru/network-page.json b/public/locales/ru/network-page.json index 912ff45..8fca17d 100644 --- a/public/locales/ru/network-page.json +++ b/public/locales/ru/network-page.json @@ -53,5 +53,6 @@ "rank": "Ранг", "addressID": "ID адреса", "richListLoadFailed": "Ошибка: Не удалось загрузить список богатых. Пожалуйста, обновите страницу или попробуйте позже.", - "richListWarning": "Данные списка богатых обновляются в начале каждой эпохи" + "richListWarning": "Данные списка богатых обновляются в начале каждой эпохи", + "timestamp": "Метка времени" } diff --git a/public/locales/tr/network-page.json b/public/locales/tr/network-page.json index e84cfcd..5c631c4 100644 --- a/public/locales/tr/network-page.json +++ b/public/locales/tr/network-page.json @@ -53,5 +53,6 @@ "rank": "Rütbe", "addressID": "Adres Kimliği", "richListLoadFailed": "Hata: Zenginler listesi yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.", - "richListWarning": "Zenginler listesi verileri her dönemin başında güncellenir" + "richListWarning": "Zenginler listesi verileri her dönemin başında güncellenir", + "timestamp": "Zaman damgası" } diff --git a/public/locales/zh/network-page.json b/public/locales/zh/network-page.json index 72117fb..f95533b 100644 --- a/public/locales/zh/network-page.json +++ b/public/locales/zh/network-page.json @@ -53,5 +53,6 @@ "rank": "排名", "addressID": "地址标识", "richListLoadFailed": "错误:无法加载富豪榜。请刷新页面或稍后再试。", - "richListWarning": "富豪榜数据会在每个纪元开始时更新" + "richListWarning": "富豪榜数据会在每个纪元开始时更新", + "timestamp": "时间戳" } diff --git a/src/components/ui/Breadcrumbs.tsx b/src/components/ui/Breadcrumbs.tsx index 6b5844c..970760d 100644 --- a/src/components/ui/Breadcrumbs.tsx +++ b/src/components/ui/Breadcrumbs.tsx @@ -1,10 +1,10 @@ -import { Children, Fragment } from 'react' +import { Children, Fragment, memo } from 'react' type Props = { children?: React.ReactNode | React.ReactNode[] } -export default function Breadcrumbs({ children }: Props) { +function Breadcrumbs({ children }: Props) { return ( ) } + +const MemoizedBreadcrumbs = memo(Breadcrumbs) + +export default MemoizedBreadcrumbs diff --git a/src/components/ui/InfiniteScroll.tsx b/src/components/ui/InfiniteScroll.tsx index 9666165..b6914ef 100644 --- a/src/components/ui/InfiniteScroll.tsx +++ b/src/components/ui/InfiniteScroll.tsx @@ -3,8 +3,8 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { ArrowUpIcon } from '@app/assets/icons' import { Alert } from '@app/components/ui' +import { DotsLoader } from '@app/components/ui/loaders' import { clsxTwMerge } from '@app/utils' -import { DotsLoader } from './loaders' interface InfiniteScrollProps { items: T[] // Array of items to display @@ -36,8 +36,8 @@ export default function InfiniteScroll({ const [internalError, setInternalError] = useState(null) const [showScrollToTop, setShowScrollToTop] = useState(false) - const isLoading = externalIsLoading !== undefined ? externalIsLoading : internalIsLoading - const error = externalError !== undefined ? externalError : internalError + const isLoading = externalIsLoading ?? internalIsLoading + const error = externalError ?? internalError const handleLoadMore = useCallback(async () => { try { @@ -69,22 +69,29 @@ export default function InfiniteScroll({ [handleLoadMore, hasMore, isLoading, threshold] ) - const handleScrollToTop = () => { + const handleScrollToTop = useCallback(() => { window.scrollTo({ top: 0, behavior: 'smooth' }) - } + }, []) - useEffect(() => { - const handleScroll = () => { - if (window.scrollY > window.innerHeight) { - setShowScrollToTop(true) - } else { - setShowScrollToTop(false) - } + const handleScroll = useCallback(() => { + if (window.scrollY > window.innerHeight) { + setShowScrollToTop(true) + } else { + setShowScrollToTop(false) } + }, []) + useEffect(() => { window.addEventListener('scroll', handleScroll) return () => window.removeEventListener('scroll', handleScroll) - }, []) + }, [handleScroll]) + + const renderStatus = useCallback(() => { + if (error) return {error} + if (isLoading) return loader + if (!hasMore && endMessage) return endMessage + return null + }, [error, isLoading, loader, hasMore, endMessage]) return (
@@ -98,20 +105,15 @@ export default function InfiniteScroll({ ))} - {isLoading && loader} - {!hasMore && !isLoading && endMessage} - {error && ( - - {error} - - )} + + {renderStatus()} {showScrollToTop && ( diff --git a/src/components/ui/SearchBar/SearchBar.tsx b/src/components/ui/SearchBar/SearchBar.tsx index 5273230..e690111 100644 --- a/src/components/ui/SearchBar/SearchBar.tsx +++ b/src/components/ui/SearchBar/SearchBar.tsx @@ -90,7 +90,7 @@ export default function SearchBar() { closeOnOutsideClick onClose={handleCloseCallback} > -
+
{isLoading && (
diff --git a/src/components/ui/layouts/Footer.tsx b/src/components/ui/layouts/Footer.tsx index 3a870f6..53ddfd3 100644 --- a/src/components/ui/layouts/Footer.tsx +++ b/src/components/ui/layouts/Footer.tsx @@ -51,7 +51,7 @@ function Footer() { ))}
-

Version 1.5

+

Version 1.5.1

) } diff --git a/src/pages/network/OverviewPage.tsx b/src/pages/network/OverviewPage.tsx index 8d7016d..d0d36b9 100644 --- a/src/pages/network/OverviewPage.tsx +++ b/src/pages/network/OverviewPage.tsx @@ -13,7 +13,7 @@ import { StarsIcon, WalletIcon } from '@app/assets/icons' -import { PaginationBar } from '@app/components/ui' +import { PaginationBar, Tooltip } from '@app/components/ui' import { LinearProgress } from '@app/components/ui/loaders' import { useAppDispatch, useAppSelector } from '@app/hooks/redux' import { getOverview, selectOverview } from '@app/store/network/overviewSlice' @@ -105,9 +105,11 @@ export default function OverviewPage() { id: 'empty-ticks', icon: EmptyTicksIcon, label: ( - + {t('empty')} - + + + ), value: formatString(overview?.numberOfEmptyTicks) diff --git a/src/pages/network/TickPage.tsx b/src/pages/network/TickPage.tsx index d99a2e7..07ac543 100644 --- a/src/pages/network/TickPage.tsx +++ b/src/pages/network/TickPage.tsx @@ -94,7 +94,7 @@ export default function TickPage() {
-
-

{t('transactions')}

- - - {t('latest')} - {t('historical')} - - - - - - - - - - + {detailsOpen && }
+
) } diff --git a/src/pages/network/address/components/AddressDetails.tsx b/src/pages/network/address/components/AddressDetails.tsx new file mode 100644 index 0000000..bda8645 --- /dev/null +++ b/src/pages/network/address/components/AddressDetails.tsx @@ -0,0 +1,53 @@ +import type { Address } from '@app/store/network/addressSlice' +import { formatString } from '@app/utils' +import { useTranslation } from 'react-i18next' +import { CardItem, TickLink } from '../../components' + +type Props = { + address: Address +} + +export default function AddressDetails({ address }: Props) { + const { t } = useTranslation('network-page') + + return ( +
+ {(Object.entries(address.reportedValues) || []).map(([ip, details]) => ( + +
+
+

{t('value')}

+

+ {formatString(details.incomingAmount - details.outgoingAmount)}{' '} + QUBIC +

+
+

{ip}

+
+
+

+ {t('incoming')}: + {details.numberOfIncomingTransfers} ( + {t('latest')}:{' '} + + ) +

+

+ {t('outgoing')}: + {details.numberOfOutgoingTransfers} ( + {t('latest')}:{' '} + + ) +

+
+
+ ))} +
+ ) +} diff --git a/src/pages/network/address/components/Transactions.tsx b/src/pages/network/address/components/Transactions.tsx deleted file mode 100644 index b15d0cc..0000000 --- a/src/pages/network/address/components/Transactions.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' - -import { InfiniteScroll } from '@app/components/ui' -import { DotsLoader } from '@app/components/ui/loaders' -import { useAppDispatch, useAppSelector } from '@app/hooks/redux' -import type { Address, TransactionWithMoneyFlew } from '@app/store/network/addressSlice' -import { getTransferTxs, selectTransferTxs } from '@app/store/network/addressSlice' -import { TxItem } from '../../components' - -type Props = { - addressId: string - address: Address -} - -export const BATCH_SIZE = 50 - -export const TICK_SIZE = 100_000 - -export default function Transactions({ addressId, address }: Props) { - const { t } = useTranslation('network-page') - const dispatch = useAppDispatch() - const { - data: transferTxs, - isLoading, - error, - hasMore, - lastStartTick, - lastEndTick - } = useAppSelector(selectTransferTxs) - const [displayTransferTxs, setDisplayTransferTxs] = useState([]) - - const loadMore = useCallback(() => { - const remainingTxs = transferTxs.slice( - displayTransferTxs.length, - displayTransferTxs.length + BATCH_SIZE - ) - if (remainingTxs.length >= BATCH_SIZE) { - setDisplayTransferTxs((prev) => [...prev, ...remainingTxs]) - } else if (!isLoading && hasMore) { - const newEndTick = Math.max(0, lastEndTick - 1 - TICK_SIZE) - const newStartTick = Math.max(0, lastStartTick - 1 - TICK_SIZE) - dispatch(getTransferTxs({ addressId, startTick: newStartTick, endTick: newEndTick })) - } - }, [ - transferTxs, - displayTransferTxs.length, - isLoading, - hasMore, - lastEndTick, - lastStartTick, - dispatch, - addressId - ]) - - useEffect(() => { - if (!transferTxs.length && address.endTick) { - dispatch( - getTransferTxs({ - addressId, - startTick: address.endTick - TICK_SIZE, - endTick: address.endTick - }) - ) - } - }, [address.endTick, addressId, dispatch, transferTxs.length]) - - useEffect(() => { - if (transferTxs.length > 0) { - setDisplayTransferTxs((prev) => [ - ...prev, - ...transferTxs.slice(prev.length, prev.length + BATCH_SIZE) - ]) - } - }, [transferTxs]) - - return ( - } - error={error && t('loadingTransactionsError')} - endMessage={ -

- {displayTransferTxs.length === 0 ? t('noTransactions') : t('allTransactionsLoaded')} -

- } - renderItem={(tx: TransactionWithMoneyFlew) => ( - - )} - /> - ) -} diff --git a/src/pages/network/address/components/HistoricalTxs.tsx b/src/pages/network/address/components/TransactionsOverview/HistoricalTxs.tsx similarity index 79% rename from src/pages/network/address/components/HistoricalTxs.tsx rename to src/pages/network/address/components/TransactionsOverview/HistoricalTxs.tsx index 92fd6d1..ec48c26 100644 --- a/src/pages/network/address/components/HistoricalTxs.tsx +++ b/src/pages/network/address/components/TransactionsOverview/HistoricalTxs.tsx @@ -3,9 +3,10 @@ import { InfiniteScroll } from '@app/components/ui' import { DotsLoader } from '@app/components/ui/loaders' import { useAppDispatch, useAppSelector } from '@app/hooks/redux' import { getHistoricalTxs, selectHistoricalTxs } from '@app/store/network/addressSlice' +import type { TransactionWithStatus } from '@app/types' import { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { TxItem } from '../../components' +import { TxItem } from '../../../components' type Props = { addressId: string @@ -20,6 +21,20 @@ export default function HistoricalTxs({ addressId }: Props) { dispatch(getHistoricalTxs(addressId)) }, [dispatch, addressId]) + const renderTxItem = useCallback( + ({ tx, status }: TransactionWithStatus) => ( + + ), + [addressId] + ) + useEffect(() => { if (historicalTxs.length === 0) { loadMoreTxs() @@ -47,16 +62,7 @@ export default function HistoricalTxs({ addressId }: Props) { {historicalTxs.length === 0 ? t('noTransactions') : t('allTransactionsLoaded')}

} - renderItem={({ tx, status }) => ( - - )} + renderItem={renderTxItem} />
) diff --git a/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx b/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx new file mode 100644 index 0000000..ec776a3 --- /dev/null +++ b/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx @@ -0,0 +1,58 @@ +import { useTranslation } from 'react-i18next' + +import { InfiniteScroll } from '@app/components/ui' +import { DotsLoader } from '@app/components/ui/loaders' +import type { TransactionV2 } from '@app/store/apis/archiver-v2.types' +import { useCallback } from 'react' +import { TxItem } from '../../../components' + +type Props = { + addressId: string + transactions: TransactionV2[] + loadMore: () => Promise + hasMore: boolean + isLoading: boolean + error: string | null +} + +export default function LatestTransactions({ + addressId, + transactions, + loadMore, + hasMore, + isLoading, + error +}: Props) { + const { t } = useTranslation('network-page') + + const renderTxItem = useCallback( + ({ transaction, moneyFlew, timestamp }: TransactionV2) => ( + + ), + [addressId] + ) + + return ( + } + error={error && t('loadingTransactionsError')} + endMessage={ +

+ {transactions.length === 0 ? t('noTransactions') : t('allTransactionsLoaded')} +

+ } + renderItem={renderTxItem} + /> + ) +} diff --git a/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx b/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx new file mode 100644 index 0000000..fde35d6 --- /dev/null +++ b/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx @@ -0,0 +1,51 @@ +import { useTranslation } from 'react-i18next' + +import { Tabs } from '@app/components/ui' +import type { Address } from '@app/store/network/addressSlice' +import { memo } from 'react' +import { useLatestTransactions } from '../../hooks' +import HistoricalTxs from './HistoricalTxs' +import LatestTransactions from './LatestTransactions' + +type Props = { + address: Address + addressId: string +} + +function TransactionsOverview({ address, addressId }: Props) { + const { t } = useTranslation('network-page') + const { transactions, loadMoreTransactions, hasMore, isLoading, error } = useLatestTransactions( + addressId, + address.endTick + ) + + return ( +
+

{t('transactions')}

+ + + {t('latest')} + {t('historical')} + + + + + + + + + + +
+ ) +} + +const MemoizedTransactionsOverview = memo(TransactionsOverview) +export default MemoizedTransactionsOverview diff --git a/src/pages/network/address/components/index.ts b/src/pages/network/address/components/index.ts index a1ef3bd..4411351 100644 --- a/src/pages/network/address/components/index.ts +++ b/src/pages/network/address/components/index.ts @@ -1,2 +1,2 @@ -export { default as HistoricalTxs } from './HistoricalTxs' -export { default as Transactions } from './Transactions' +export { default as AddressDetails } from './AddressDetails' +export { default as TransactionsOverview } from './TransactionsOverview/TransactionsOverview' diff --git a/src/pages/network/address/hooks/index.ts b/src/pages/network/address/hooks/index.ts new file mode 100644 index 0000000..7a2af43 --- /dev/null +++ b/src/pages/network/address/hooks/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line +export { default as useLatestTransactions } from './useLatestTransactions' diff --git a/src/pages/network/address/hooks/useLatestTransactions.ts b/src/pages/network/address/hooks/useLatestTransactions.ts new file mode 100644 index 0000000..d0f5fb3 --- /dev/null +++ b/src/pages/network/address/hooks/useLatestTransactions.ts @@ -0,0 +1,129 @@ +import { useLazyGetIndentityTransfersQuery } from '@app/store/apis/archiver-v2.api' +import type { TransactionV2 } from '@app/store/apis/archiver-v2.types' +import type { Address } from '@app/store/network/addressSlice' +import { useCallback, useEffect, useState } from 'react' + +const BATCH_SIZE = 50 +const TICK_SIZE = 200_000 + +export interface UseLatestTransactionsResult { + transactions: TransactionV2[] + loadMoreTransactions: () => Promise + hasMore: boolean + isLoading: boolean + error: string | null +} + +export default function useLatestTransactions( + addressId: string, + addressEndTick: Address['endTick'] +): UseLatestTransactionsResult { + const [startTick, setStartTick] = useState(Math.max(0, addressEndTick - TICK_SIZE)) + const [transactions, setTransactions] = useState([]) + const [txsList, setTxsList] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [getIdentityTransfersQuery, { isFetching, error }] = useLazyGetIndentityTransfersQuery({}) + + const hasMore = startTick > 0 + + const fetchTransfers = useCallback( + async (start: number, end: number) => { + const result = await getIdentityTransfersQuery({ + addressId, + startTick: start, + endTick: end + }).unwrap() + + return result || [] + }, + [getIdentityTransfersQuery, addressId] + ) + + const fetchRecursive = useCallback( + async (start: number, end: number, accumulatedData: TransactionV2[] = []) => { + const newTxs = await fetchTransfers(start, end) + const combinedData = [...new Set(accumulatedData.concat(newTxs))] + + if (combinedData.length < BATCH_SIZE && start > 0) { + const newEndTick = Math.max(0, start - 1) + const newStartTick = Math.max(0, start - 1 - TICK_SIZE) + return fetchRecursive(newStartTick, newEndTick, combinedData) + } + + return { + newTxs: combinedData.sort((a, b) => b.transaction.tickNumber - a.transaction.tickNumber), + lastStartTick: start + } + }, + [fetchTransfers] + ) + + const loadMoreTransactions = useCallback(async () => { + if (isLoading || isFetching || !hasMore) return + + setIsLoading(true) + try { + if (txsList.length < BATCH_SIZE) { + const newStartTick = Math.max(0, startTick - 1 - TICK_SIZE) + const newEndTick = Math.max(0, startTick - 1) + const { newTxs, lastStartTick } = await fetchRecursive(newStartTick, newEndTick) + // Since there could be some txs in txsList already, we need to merge them and then slice it + const updatedTxList = [...txsList, ...newTxs] + // Adding the new transactions to the list to be displayed + setTransactions((prev) => [...prev, ...updatedTxList.slice(0, BATCH_SIZE)]) + // Updating the list of remaining transactions + setTxsList(updatedTxList.slice(BATCH_SIZE, updatedTxList.length)) + // Updating the start and end tick + setStartTick(lastStartTick) + } else { + setTransactions((prev) => [...prev, ...txsList.slice(0, BATCH_SIZE)]) + setTxsList((prevTxsList) => prevTxsList.slice(BATCH_SIZE, prevTxsList.length)) + } + } finally { + setIsLoading(false) + } + }, [startTick, fetchRecursive, isLoading, isFetching, hasMore, txsList]) + + useEffect(() => { + let isMounted = true + + const initialFetch = async () => { + setIsLoading(true) + const initialStartTick = Math.max(0, addressEndTick - TICK_SIZE) + const { newTxs, lastStartTick } = await fetchRecursive(initialStartTick, addressEndTick) + + if (isMounted) { + setTransactions(newTxs.slice(0, BATCH_SIZE)) + setTxsList(newTxs.slice(BATCH_SIZE, newTxs.length)) + setStartTick(lastStartTick) + setIsLoading(false) + } + } + + if (transactions.length === 0 && addressEndTick) { + initialFetch() + } + + return () => { + isMounted = false + } + }, [fetchRecursive, transactions.length, addressEndTick]) + + useEffect(() => { + return () => { + if (addressId) { + setTransactions([]) + setTxsList([]) + setStartTick(0) + } + } + }, [addressId]) + + return { + transactions, + loadMoreTransactions, + hasMore, + isLoading, + error: error ? String(error) : null + } +} diff --git a/src/pages/network/components/OverviewCardItem.tsx b/src/pages/network/components/OverviewCardItem.tsx index 96d21cb..42aa7c8 100644 --- a/src/pages/network/components/OverviewCardItem.tsx +++ b/src/pages/network/components/OverviewCardItem.tsx @@ -14,18 +14,20 @@ export default function OverviewCardItem({ value: string variant?: 'normal' | 'small' }) { + const LabelTag = typeof label === 'string' ? 'p' : 'div' + return (
- +
-

{label}

-

{value}

+ {label} +

{value}

diff --git a/src/pages/network/components/TxItem/TransactionDetails.tsx b/src/pages/network/components/TxItem/TransactionDetails.tsx index 93816d2..6829aa3 100644 --- a/src/pages/network/components/TxItem/TransactionDetails.tsx +++ b/src/pages/network/components/TxItem/TransactionDetails.tsx @@ -1,8 +1,9 @@ import { useTranslation } from 'react-i18next' import type { Transaction } from '@app/services/archiver' -import { formatString } from '@app/utils' +import { formatDate, formatString } from '@app/utils' import type { Transfer } from '@app/utils/qubic-ts' +import { useMemo } from 'react' import AddressLink from '../AddressLink' import SubCardItem from '../SubCardItem' import TickLink from '../TickLink' @@ -15,6 +16,7 @@ type Props = { entries: Transfer[] isHistoricalTx?: boolean variant?: TxItemVariant + timestamp?: string } function TransactionDetailsWrapper({ @@ -37,11 +39,13 @@ export default function TransactionDetails({ txDetails: { txId, sourceId, tickNumber, destId, inputType, amount }, entries, isHistoricalTx = false, + timestamp, variant = 'primary' }: Props) { const { t } = useTranslation('network-page') const isSecondaryVariant = variant === 'secondary' + const { date, time } = useMemo(() => formatDate(timestamp, { split: true }), [timestamp]) return ( @@ -105,6 +109,19 @@ export default function TransactionDetails({ /> )} + {timestamp && ( + + {date}{' '} + {time} +

+ } + /> + )} +
) diff --git a/src/pages/network/components/TxItem/TxItem.tsx b/src/pages/network/components/TxItem/TxItem.tsx index 44f37cb..8ee33b7 100644 --- a/src/pages/network/components/TxItem/TxItem.tsx +++ b/src/pages/network/components/TxItem/TxItem.tsx @@ -15,18 +15,20 @@ import type { TxItemVariant } from './TxItem.types' type Props = { tx: Omit - identify?: string + identity?: string nonExecutedTxIds: string[] variant?: TxItemVariant isHistoricalTx?: boolean + timestamp?: string } function TxItem({ tx: { txId, sourceId, tickNumber, destId, inputType, amount, inputHex }, - identify, + identity, nonExecutedTxIds, variant = 'primary', - isHistoricalTx = false + isHistoricalTx = false, + timestamp }: Props) { const [entries, setEntries] = useState([]) const [detailsOpen, setDetailsOpen] = useState(false) @@ -69,6 +71,7 @@ function TxItem({ isHistoricalTx={isHistoricalTx} variant={variant} entries={entries} + timestamp={timestamp} /> ) @@ -82,16 +85,16 @@ function TxItem({ isTransferTx={isTransferTransaction} />
- {identify ? ( + {identity ? (
- {identify === sourceId ? ( + {identity === sourceId ? ( ) : ( )} @@ -121,6 +124,7 @@ function TxItem({ isHistoricalTx={isHistoricalTx} variant={variant} entries={entries} + timestamp={timestamp} /> )} diff --git a/src/services/archiver/endpoints.ts b/src/services/archiver/endpoints.ts index 93874cc..2fb0db8 100644 --- a/src/services/archiver/endpoints.ts +++ b/src/services/archiver/endpoints.ts @@ -1,6 +1,6 @@ import { envConfig } from '@app/configs' -const BASE_URL = envConfig.ARCHIVER_API_URL +const BASE_URL = `${envConfig.ARCHIVER_API_URL}/v1` const formatTick = (tick: string) => parseInt(tick.replace(/,/g, ''), 10) diff --git a/src/store/apis/archiver-v2.api.ts b/src/store/apis/archiver-v2.api.ts new file mode 100644 index 0000000..462fe27 --- /dev/null +++ b/src/store/apis/archiver-v2.api.ts @@ -0,0 +1,34 @@ +import { envConfig } from '@app/configs' +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import type { + GetIdentityTransfersArgs, + GetIdentityTransfersResponse, + GetTransactionResponse +} from './archiver-v2.types' + +const BASE_URL = `${envConfig.ARCHIVER_API_URL}/v2` + +export const archiverV2Api = createApi({ + reducerPath: 'archiverV2Api', + baseQuery: fetchBaseQuery({ baseUrl: BASE_URL }), + endpoints: (builder) => ({ + getTransaction: builder.query({ + query: (txId) => `transactions/${txId}` + }), + getIndentityTransfers: builder.query< + GetIdentityTransfersResponse['transactions'][0]['transactions'], + GetIdentityTransfersArgs + >({ + query: ({ addressId, startTick, endTick }) => + `identities/${addressId}/transfers?startTick=${startTick}&endTick=${endTick}`, + transformResponse: (response: GetIdentityTransfersResponse) => + response.transactions.flatMap(({ transactions }) => transactions) + }) + }) +}) + +export const { + useGetTransactionQuery, + useGetIndentityTransfersQuery, + useLazyGetIndentityTransfersQuery +} = archiverV2Api diff --git a/src/store/apis/archiver-v2.types.ts b/src/store/apis/archiver-v2.types.ts new file mode 100644 index 0000000..816e311 --- /dev/null +++ b/src/store/apis/archiver-v2.types.ts @@ -0,0 +1,23 @@ +import type { Transaction } from '@app/services/archiver' + +export interface TransactionV2 { + transaction: Transaction + timestamp: string + moneyFlew: boolean +} + +export type GetTransactionResponse = TransactionV2 + +export interface GetIdentityTransfersArgs { + addressId: string + startTick: number + endTick: number +} + +export interface GetIdentityTransfersResponse { + transactions: { + identity: string + tickNumber: number + transactions: TransactionV2[] + }[] +} diff --git a/src/store/index.ts b/src/store/index.ts index 46f1562..6f01fa2 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,4 +1,5 @@ import { configureStore } from '@reduxjs/toolkit' +import { archiverV2Api } from './apis/archiver-v2.api' import localeReducer from './localeSlice' import { networkReducer } from './network/networkReducer' import searchReducer from './searchSlice' @@ -7,8 +8,11 @@ export const store = configureStore({ reducer: { locale: localeReducer, search: searchReducer, - network: networkReducer - } + network: networkReducer, + [archiverV2Api.reducerPath]: archiverV2Api.reducer + }, + + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(archiverV2Api.middleware) }) export type RootState = ReturnType diff --git a/src/store/network/adapters/convertHistoricalTxToTxWithStatus.ts b/src/store/network/adapters/convertHistoricalTxToTxWithStatus.ts index 3e0aff9..cd47019 100644 --- a/src/store/network/adapters/convertHistoricalTxToTxWithStatus.ts +++ b/src/store/network/adapters/convertHistoricalTxToTxWithStatus.ts @@ -1,7 +1,6 @@ import type { HistoricalTx } from '@app/services/qli' -import { TxTypeEnum } from '@app/types' -import { isTransferTx } from '@app/utils/qubic-ts' -import type { TransactionWithStatus } from '../txSlice' +import { type TransactionWithStatus } from '@app/types' +import { getTxType } from '@app/utils' import convertHistoricalTxToLatestTx from './convertHistoricalTxToLatestTx' export default function convertHistoricalTxToTxWithStatus( @@ -12,9 +11,7 @@ export default function convertHistoricalTxToTxWithStatus( status: { txId: historicalTx.id, moneyFlew: historicalTx.moneyFlew, - txType: isTransferTx(historicalTx.sourceId, historicalTx.destId, historicalTx.amount) - ? TxTypeEnum.TRANSFER - : TxTypeEnum.PROTOCOL + txType: getTxType(historicalTx) } } } diff --git a/src/store/network/adapters/convertTxV2ToTxWithStatus.ts b/src/store/network/adapters/convertTxV2ToTxWithStatus.ts new file mode 100644 index 0000000..5106d4e --- /dev/null +++ b/src/store/network/adapters/convertTxV2ToTxWithStatus.ts @@ -0,0 +1,15 @@ +import type { TransactionV2 } from '@app/store/apis/archiver-v2.types' +import type { TransactionWithStatus } from '@app/types' +import { getTxType } from '@app/utils' + +export default function convertTxV2ToTxWithStatus(tx: TransactionV2): TransactionWithStatus { + return { + tx: tx.transaction, + status: { + txId: tx.transaction.txId, + moneyFlew: tx.moneyFlew, + txType: getTxType(tx.transaction) + }, + timestamp: tx.timestamp + } +} diff --git a/src/store/network/adapters/index.ts b/src/store/network/adapters/index.ts index 7c588ce..88df7cb 100644 --- a/src/store/network/adapters/index.ts +++ b/src/store/network/adapters/index.ts @@ -1,2 +1,3 @@ export { default as convertHistoricalTxToLatestTx } from './convertHistoricalTxToLatestTx' export { default as convertHistoricalTxToTxWithStatus } from './convertHistoricalTxToTxWithStatus' +export { default as convertTxV2ToTxWithStatus } from './convertTxV2ToTxWithStatus' diff --git a/src/store/network/addressSlice.ts b/src/store/network/addressSlice.ts index 408a1e7..8948b65 100644 --- a/src/store/network/addressSlice.ts +++ b/src/store/network/addressSlice.ts @@ -1,21 +1,13 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' -import { BATCH_SIZE, TICK_SIZE } from '@app/pages/network/address/components/Transactions' -import type { Balance, Transaction } from '@app/services/archiver' +import type { Balance } from '@app/services/archiver' import { archiverApiService } from '@app/services/archiver' import type { ReportedValues } from '@app/services/qli' import { qliApiService } from '@app/services/qli' import type { RootState } from '@app/store' -import { TxTypeEnum } from '@app/types' +import type { TransactionWithStatus } from '@app/types' import { handleThunkError } from '@app/utils/error-handlers' -import { isTransferTx } from '@app/utils/qubic-ts' import { convertHistoricalTxToTxWithStatus } from './adapters' -import type { TransactionWithStatus } from './txSlice' - -export type TransactionWithMoneyFlew = Transaction & { - moneyFlew: boolean | null - txType: TxTypeEnum -} export const getAddress = createAsyncThunk( 'network/address', @@ -27,6 +19,7 @@ export const getAddress = createAsyncThunk( archiverApiService.getBalance(addressId) ]) return { + addressId, reportedValues, endTick: lastProcessedTick.tickNumber, balance @@ -37,76 +30,6 @@ export const getAddress = createAsyncThunk( } ) -export const getTransferTxs = createAsyncThunk< - { - data: TransactionWithMoneyFlew[] - lastStartTick: number - lastEndTick: number - }, - { addressId: string; startTick: number; endTick: number }, - { - state: RootState - } ->( - 'network/getTransferTxs', - async ({ addressId, startTick, endTick }, { rejectWithValue }) => { - try { - let data: Transaction[] = [] - let lastStartTick = startTick - let lastEndTick = endTick - - const getTransfers = async (start: number, end: number) => { - const { transferTransactionsPerTick } = - await archiverApiService.getAddressTransferTransactions(addressId, start, end) - return transferTransactionsPerTick.flatMap(({ transactions }) => transactions) || [] - } - const fetchRecursive = async (start: number, end: number) => { - const transfers = await getTransfers(start, end) - data = [...new Set(data.concat(transfers))] - - if (start === 0 && transfers.length === 0) { - return { data: data.sort((a, b) => b.tickNumber - a.tickNumber) } - } - - if (data.length < BATCH_SIZE) { - lastEndTick = Math.max(0, start - 1) - lastStartTick = Math.max(0, lastEndTick - TICK_SIZE) - - return fetchRecursive(lastStartTick, lastEndTick) - } - return { data: data.sort((a, b) => b.tickNumber - a.tickNumber) } - } - - const finalResult = await fetchRecursive(startTick, endTick) - - const txsWithMoneyFlew = await Promise.all( - finalResult.data.map(async (tx) => { - if (!isTransferTx(tx.sourceId, tx.destId, tx.amount)) { - return { ...tx, moneyFlew: true, txType: TxTypeEnum.PROTOCOL } - } - try { - const { transactionStatus } = await archiverApiService.getTransactionStatus(tx.txId) - return { ...tx, moneyFlew: transactionStatus.moneyFlew, txType: TxTypeEnum.TRANSFER } - } catch (error) { - return { ...tx, moneyFlew: null, txType: TxTypeEnum.TRANSFER } - } - }) - ) - - return { data: txsWithMoneyFlew, lastStartTick, lastEndTick } - } catch (error) { - return rejectWithValue(handleThunkError(error)) - } - }, - // Conditionally fetch historical transactions to prevent issues from React.StrictMode - https://redux.js.org/tutorials/essentials/part-5-async-logic#avoiding-duplicate-fetches - { - condition: (_, { getState }) => { - const { isLoading, hasMore, error } = getState().network.address.transferTxs - return !isLoading && hasMore && !error - } - } -) - export const getHistoricalTxs = createAsyncThunk< TransactionWithStatus[], string, @@ -134,6 +57,7 @@ export const getHistoricalTxs = createAsyncThunk< ) export type Address = { + addressId: string reportedValues: ReportedValues endTick: number balance: Balance @@ -143,14 +67,6 @@ export interface AddressState { address: Address | null isLoading: boolean error: string | null - transferTxs: { - data: TransactionWithMoneyFlew[] - isLoading: boolean - error: string | null - hasMore: boolean - lastStartTick: number - lastEndTick: number - } historicalTxs: { data: TransactionWithStatus[] isLoading: boolean @@ -164,14 +80,6 @@ const initialState: AddressState = { address: null, isLoading: false, error: null, - transferTxs: { - data: [], - isLoading: false, - error: null, - hasMore: true, - lastStartTick: 0, - lastEndTick: 0 - }, historicalTxs: { data: [], isLoading: false, @@ -204,26 +112,6 @@ const addressSlice = createSlice({ state.isLoading = false state.error = action.error.message ?? 'Unknown error' }) - // getTransferTxs - .addCase(getTransferTxs.pending, (state) => { - state.transferTxs.isLoading = true - state.transferTxs.error = null - }) - .addCase(getTransferTxs.fulfilled, (state, action) => { - state.transferTxs.isLoading = false - const newTxs = action.payload - if (newTxs.data.length === 0) { - state.transferTxs.hasMore = false - } else { - state.transferTxs.data.push(...newTxs.data) - } - state.transferTxs.lastStartTick = newTxs.lastStartTick - state.transferTxs.lastEndTick = newTxs.lastEndTick - }) - .addCase(getTransferTxs.rejected, (state, action) => { - state.transferTxs.isLoading = false - state.transferTxs.error = action.error.message ?? 'Unknown error' - }) // getHistoricalTxs .addCase(getHistoricalTxs.pending, (state) => { state.historicalTxs.isLoading = true @@ -249,7 +137,6 @@ const addressSlice = createSlice({ // Selectors export const selectAddress = (state: RootState) => state.network.address -export const selectTransferTxs = (state: RootState) => state.network.address.transferTxs export const selectHistoricalTxs = (state: RootState) => state.network.address.historicalTxs // actions diff --git a/src/store/network/txSlice.ts b/src/store/network/txSlice.ts index c690dd5..d11d868 100644 --- a/src/store/network/txSlice.ts +++ b/src/store/network/txSlice.ts @@ -1,18 +1,10 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' -import type { Transaction, TransactionStatus } from '@app/services/archiver' -import { archiverApiService } from '@app/services/archiver' import { qliApiService } from '@app/services/qli' import type { RootState } from '@app/store' -import { TxTypeEnum, type TxEra, type TxType } from '@app/types' -import { isTransferTx } from '@app/utils/qubic-ts' +import type { TransactionWithStatus, TxEra } from '@app/types' import { convertHistoricalTxToTxWithStatus } from './adapters' -export type TransactionWithStatus = { - tx: Transaction - status: TransactionStatus & { txType: TxType } -} - export interface TxState { txWithStatus: TransactionWithStatus | null isLoading: boolean @@ -31,7 +23,7 @@ type GetTxArgs = { } export const getTx = createAsyncThunk< - TransactionWithStatus, + TransactionWithStatus | null, GetTxArgs, { state: RootState @@ -54,19 +46,7 @@ export const getTx = createAsyncThunk< return historicalTx } - const { transaction } = await archiverApiService.getTransaction(txId) - const { transactionStatus } = isTransferTx( - transaction.sourceId, - transaction.destId, - transaction.amount - ) - ? await archiverApiService.getTransactionStatus(txId) - : { transactionStatus: { txId, moneyFlew: false, txType: TxTypeEnum.PROTOCOL } } - - return { - tx: transaction, - status: { ...transactionStatus, txType: TxTypeEnum.TRANSFER } - } + return null }) const txSlice = createSlice({ diff --git a/src/types/index.ts b/src/types/index.ts index a19692f..e10aa1c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ +import type { Transaction, TransactionStatus } from '@app/services/archiver' + export type Language = { id: string label: string @@ -11,3 +13,9 @@ export enum TxTypeEnum { } export type TxType = keyof typeof TxTypeEnum + +export type TransactionWithStatus = { + tx: Transaction + status: TransactionStatus & { txType: TxType } + timestamp?: string +} diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 0000000..c12daa7 --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,37 @@ +// eslint-disable-next-line import/prefer-default-export -- Remove this comment when adding more functions +export function formatDate( + dateString: string | undefined, + options?: { split?: T } +): T extends true ? { date: string; time: string } : string { + const defaultResult = (options?.split ? { date: '', time: '' } : '') as T extends true + ? { date: string; time: string } + : string + const formatDateTime = (date: Date, dateTimeFormatOptions: Intl.DateTimeFormatOptions) => + new Intl.DateTimeFormat('en-US', dateTimeFormatOptions).format(date) + + if (!dateString) return defaultResult + + const dateOptions: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric' + } + + const timeOptions: Intl.DateTimeFormatOptions = { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + hour12: true + } + + const date = new Date(dateString.includes('T') ? dateString : parseInt(dateString, 10)) + + if (Number.isNaN(date.getTime())) return defaultResult + + return ( + options?.split + ? { date: formatDateTime(date, dateOptions), time: formatDateTime(date, timeOptions) } + : formatDateTime(date, { ...dateOptions, ...timeOptions }) + ) as T extends true ? { date: string; time: string } : string +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 92b2693..a7289fc 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -9,31 +9,6 @@ const formatString = (string: string | number | undefined | null) => { return String(string) } -const formatDate = (dateString: string | undefined) => { - const options = { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - hour12: true - } as const - - if (dateString) { - let date - if (dateString.includes('T')) { - date = new Date(dateString) - } else { - const timestamp = parseInt(dateString, 10) // Include the radix parameter - date = new Date(timestamp) - } - return new Intl.DateTimeFormat('en-US', options).format(date) // Format date - } - return '' -} - function formatEllipsis(str = '') { if (str.length > 10) { return `${str.slice(0, 5)}...${str.slice(-5)}` @@ -67,5 +42,7 @@ function copyText(textToCopy: string) { } } +export * from './date' export * from './styles' -export { copyText, formatBase64, formatDate, formatEllipsis, formatString } +export * from './transactions' +export { copyText, formatBase64, formatEllipsis, formatString } diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts new file mode 100644 index 0000000..50c0ec1 --- /dev/null +++ b/src/utils/transactions.ts @@ -0,0 +1,11 @@ +import type { TxType } from '@app/types' +import { TxTypeEnum } from '@app/types' +import { isTransferTx } from './qubic-ts' + +// eslint-disable-next-line import/prefer-default-export -- Remove this comment when adding more functions +export const getTxType = (tx: { + sourceId: string + destId: string + amount: string | number +}): TxType => + isTransferTx(tx.sourceId, tx.destId, tx.amount) ? TxTypeEnum.TRANSFER : TxTypeEnum.PROTOCOL diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 228cb6e..43902ea 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -2,6 +2,7 @@ /// interface ImportMetaEnv { + readonly VITE_ENABLE_PROXY: string readonly VITE_QLI_API_URL: string readonly VITE_ARCHIVER_API_URL: string }