/ src / modules / history / HistoryWrapper.tsx
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&apos;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;