HistoryWrapper.tsx
1 import { DocumentDownloadIcon } from '@heroicons/react/outline'; 2 import { Trans } from '@lingui/macro'; 3 import { 4 Box, 5 Button, 6 CircularProgress, 7 Paper, 8 SvgIcon, 9 Typography, 10 useMediaQuery, 11 useTheme, 12 } from '@mui/material'; 13 import React, { useCallback, useMemo, useRef, useState } from 'react'; 14 import { ConnectWalletPaper } from 'src/components/ConnectWalletPaper'; 15 import { ListWrapper } from 'src/components/lists/ListWrapper'; 16 import { SearchInput } from 'src/components/SearchInput'; 17 import { applyTxHistoryFilters, useTransactionHistory } from 'src/hooks/useTransactionHistory'; 18 import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; 19 import { useRootStore } from 'src/store/root'; 20 import { TRANSACTION_HISTORY } from 'src/utils/mixPanelEvents'; 21 22 import LandingGhost from '/public/resting-gho-hat-purple.svg'; 23 24 import { downloadData, formatTransactionData, groupByDate } from './helpers'; 25 import { HistoryFilterMenu } from './HistoryFilterMenu'; 26 import { HistoryItemLoader } from './HistoryItemLoader'; 27 import { HistoryWrapperMobile } from './HistoryWrapperMobile'; 28 import TransactionRowItem from './TransactionRowItem'; 29 import { FilterOptions, TransactionHistoryItemUnion } from './types'; 30 31 export const HistoryWrapper = () => { 32 const [searchQuery, setSearchQuery] = useState(''); 33 const [loadingDownload, setLoadingDownload] = useState(false); 34 const [filterQuery, setFilterQuery] = useState<FilterOptions[]>([]); 35 const [searchResetKey, setSearchResetKey] = useState(0); 36 37 const isFilterActive = searchQuery.length > 0 || filterQuery.length > 0; 38 const trackEvent = useRootStore((store) => store.trackEvent); 39 40 const { 41 data: transactions, 42 isLoading, 43 fetchNextPage, 44 isFetchingNextPage, 45 fetchForDownload, 46 subgraphUrl, 47 } = useTransactionHistory({ isFilterActive }); 48 49 const handleJsonDownload = async () => { 50 trackEvent(TRANSACTION_HISTORY.DOWNLOAD, { type: 'JSON' }); 51 setLoadingDownload(true); 52 const data = await fetchForDownload({ searchQuery, filterQuery }); 53 const formattedData = formatTransactionData({ data, csv: false }); 54 const jsonData = JSON.stringify(formattedData, null, 2); 55 downloadData('transactions.json', jsonData, 'application/json'); 56 setLoadingDownload(false); 57 }; 58 59 const handleCsvDownload = async () => { 60 trackEvent(TRANSACTION_HISTORY.DOWNLOAD, { type: 'CSV' }); 61 62 setLoadingDownload(true); 63 const data: TransactionHistoryItemUnion[] = await fetchForDownload({ 64 searchQuery, 65 filterQuery, 66 }); 67 const formattedData = formatTransactionData({ data, csv: true }); 68 69 // Getting all the unique headers 70 const headersSet = new Set<string>(); 71 formattedData.forEach((transaction: TransactionHistoryItemUnion) => { 72 Object.keys(transaction).forEach((key) => headersSet.add(key)); 73 }); 74 75 const headers: string[] = Array.from(headersSet); 76 let csvContent = headers.join(',') + '\n'; 77 78 formattedData.forEach((transaction: TransactionHistoryItemUnion) => { 79 const row: string[] = headers.map((header) => { 80 const value = transaction[header as keyof TransactionHistoryItemUnion]; 81 if (typeof value === 'object') { 82 return JSON.stringify(value) ?? ''; 83 } 84 return String(value) ?? ''; 85 }); 86 csvContent += row.join(',') + '\n'; 87 }); 88 89 downloadData('transactions.csv', csvContent, 'text/csv'); 90 setLoadingDownload(false); 91 }; 92 93 const observer = useRef<IntersectionObserver | null>(null); 94 const lastElementRef = useCallback( 95 (node: HTMLDivElement | null) => { 96 if (isLoading) return; 97 if (observer.current) observer.current.disconnect(); 98 observer.current = new IntersectionObserver((entries) => { 99 if (entries[0].isIntersecting) { 100 fetchNextPage(); 101 } 102 }); 103 if (node) observer.current.observe(node); 104 }, 105 [fetchNextPage, isLoading] 106 ); 107 const theme = useTheme(); 108 const downToMD = useMediaQuery(theme.breakpoints.down('md')); 109 const { currentAccount } = useWeb3Context(); 110 111 const flatTxns = useMemo( 112 () => transactions?.pages?.flatMap((page) => page) || [], 113 [transactions] 114 ); 115 const filteredTxns = useMemo( 116 () => applyTxHistoryFilters({ searchQuery, filterQuery, txns: flatTxns }), 117 [searchQuery, filterQuery, flatTxns] 118 ); 119 120 if (!subgraphUrl) { 121 return ( 122 <Paper 123 sx={{ 124 display: 'flex', 125 flexDirection: 'column', 126 alignItems: 'center', 127 justifyContent: 'center', 128 textAlign: 'center', 129 p: 4, 130 flex: 1, 131 }} 132 > 133 <LandingGhost style={{ marginBottom: '16px' }} /> 134 <Typography variant={downToMD ? 'h4' : 'h3'}> 135 <Trans>Transaction history is not currently available for this market</Trans> 136 </Typography> 137 </Paper> 138 ); 139 } 140 141 if (!currentAccount) { 142 return ( 143 <ConnectWalletPaper 144 description={<Trans> Please connect your wallet to view transaction history.</Trans>} 145 /> 146 ); 147 } 148 149 if (downToMD) { 150 return <HistoryWrapperMobile />; 151 } 152 153 const isEmpty = filteredTxns.length === 0; 154 const filterActive = searchQuery !== '' || filterQuery.length > 0; 155 156 return ( 157 <ListWrapper 158 titleComponent={ 159 <Typography component="div" variant="h2" sx={{ mr: 4 }}> 160 <Trans>Transactions</Trans> 161 </Typography> 162 } 163 > 164 <Box sx={{ display: 'flex', justifyContent: 'space-between', mx: 8, mt: 6, mb: 4 }}> 165 <Box sx={{ display: 'inline-flex' }}> 166 <HistoryFilterMenu onFilterChange={setFilterQuery} currentFilter={filterQuery} /> 167 <SearchInput 168 onSearchTermChange={setSearchQuery} 169 placeholder="Search assets..." 170 wrapperSx={{ width: '280px' }} 171 key={searchResetKey} 172 /> 173 </Box> 174 <Box sx={{ display: 'flex', alignItems: 'center', height: 36, gap: 0.5 }}> 175 {loadingDownload && <CircularProgress size={16} sx={{ mr: 2 }} color="inherit" />} 176 <Box 177 sx={{ 178 cursor: 'pointer', 179 color: 'primary', 180 height: 'auto', 181 width: 'auto', 182 display: 'flex', 183 alignItems: 'center', 184 mr: 6, 185 }} 186 onClick={handleCsvDownload} 187 > 188 <SvgIcon> 189 <DocumentDownloadIcon width={22} height={22} /> 190 </SvgIcon> 191 <Typography variant="buttonM" color="text.primary"> 192 <Trans>.CSV</Trans> 193 </Typography> 194 </Box> 195 <Box 196 sx={{ 197 cursor: 'pointer', 198 color: 'primary', 199 height: 'auto', 200 width: 'auto', 201 display: 'flex', 202 alignItems: 'center', 203 }} 204 onClick={handleJsonDownload} 205 > 206 <SvgIcon> 207 <DocumentDownloadIcon width={22} height={22} /> 208 </SvgIcon> 209 <Typography variant="buttonM" color="text.primary"> 210 <Trans>.JSON</Trans> 211 </Typography> 212 </Box> 213 </Box> 214 </Box> 215 216 {isLoading ? ( 217 <> 218 <HistoryItemLoader /> 219 <HistoryItemLoader /> 220 </> 221 ) : !isEmpty ? ( 222 Object.entries(groupByDate(filteredTxns)).map(([date, txns], groupIndex) => ( 223 <React.Fragment key={groupIndex}> 224 <Typography variant="h4" color="text.primary" sx={{ ml: 9, mt: 6, mb: 2 }}> 225 {date} 226 </Typography> 227 {txns.map((transaction: TransactionHistoryItemUnion, index: number) => { 228 const isLastItem = index === txns.length - 1; 229 return ( 230 <div ref={isLastItem ? lastElementRef : null} key={index}> 231 <TransactionRowItem transaction={transaction as TransactionHistoryItemUnion} /> 232 </div> 233 ); 234 })} 235 </React.Fragment> 236 )) 237 ) : filterActive ? ( 238 <Box 239 sx={{ 240 display: 'flex', 241 flexDirection: 'column', 242 alignItems: 'center', 243 justifyContent: 'center', 244 textAlign: 'center', 245 p: 4, 246 flex: 1, 247 maxWidth: '468px', 248 margin: '0 auto', 249 my: 24, 250 }} 251 > 252 <Typography variant="h3" color="text.primary"> 253 <Trans>Nothing found</Trans> 254 </Typography> 255 <Typography sx={{ mt: 1, mb: 4 }} variant="description" color="text.secondary"> 256 <Trans> 257 We couldn't find any transactions related to your search. Try again with a 258 different asset name, or reset filters. 259 </Trans> 260 </Typography> 261 <Button 262 variant="outlined" 263 onClick={() => { 264 setSearchQuery(''); 265 setFilterQuery([]); 266 setSearchResetKey((prevKey) => prevKey + 1); // Remount SearchInput component to clear search query 267 }} 268 > 269 Reset Filters 270 </Button> 271 </Box> 272 ) : !isFetchingNextPage ? ( 273 <Box 274 sx={{ 275 display: 'flex', 276 flexDirection: 'column', 277 alignItems: 'center', 278 justifyContent: 'center', 279 textAlign: 'center', 280 p: 4, 281 flex: 1, 282 }} 283 > 284 <Typography sx={{ my: 24 }} variant="h3" color="text.primary"> 285 <Trans>No transactions yet.</Trans> 286 </Typography> 287 </Box> 288 ) : ( 289 <></> 290 )} 291 292 <Box 293 sx={{ display: 'flex', justifyContent: 'center', mb: isFetchingNextPage ? 6 : 0, mt: 10 }} 294 > 295 {isFetchingNextPage && ( 296 <Box 297 sx={{ 298 height: 36, 299 width: 186, 300 display: 'flex', 301 alignItems: 'center', 302 justifyContent: 'center', 303 }} 304 > 305 <CircularProgress size={20} style={{ color: '#383D51' }} /> 306 </Box> 307 )} 308 </Box> 309 </ListWrapper> 310 ); 311 }; 312 313 export default HistoryWrapper;