/ src / components / transactions / AssetInput.tsx
AssetInput.tsx
  1  import { XCircleIcon } from '@heroicons/react/solid';
  2  import { Trans } from '@lingui/macro';
  3  import {
  4    Box,
  5    BoxProps,
  6    Button,
  7    CircularProgress,
  8    FormControl,
  9    IconButton,
 10    InputBase,
 11    ListItemText,
 12    MenuItem,
 13    Select,
 14    SelectChangeEvent,
 15    Typography,
 16    useTheme,
 17  } from '@mui/material';
 18  import React, { ReactNode } from 'react';
 19  import NumberFormat, { NumberFormatProps } from 'react-number-format';
 20  import { TrackEventProps } from 'src/store/analyticsSlice';
 21  import { useRootStore } from 'src/store/root';
 22  
 23  import { CapType } from '../caps/helper';
 24  import { AvailableTooltip } from '../infoTooltips/AvailableTooltip';
 25  import { FormattedNumber } from '../primitives/FormattedNumber';
 26  import { TokenIcon } from '../primitives/TokenIcon';
 27  
 28  interface CustomProps {
 29    onChange: (event: { target: { name: string; value: string } }) => void;
 30    name: string;
 31    value: string;
 32  }
 33  
 34  export const NumberFormatCustom = React.forwardRef<NumberFormatProps, CustomProps>(
 35    function NumberFormatCustom(props, ref) {
 36      const { onChange, ...other } = props;
 37  
 38      return (
 39        <NumberFormat
 40          {...other}
 41          getInputRef={ref}
 42          onValueChange={(values) => {
 43            if (values.value !== props.value)
 44              onChange({
 45                target: {
 46                  name: props.name,
 47                  value: values.value || '',
 48                },
 49              });
 50          }}
 51          thousandSeparator
 52          isNumericString
 53          allowNegative={false}
 54        />
 55      );
 56    }
 57  );
 58  
 59  export interface Asset {
 60    balance?: string;
 61    symbol: string;
 62    iconSymbol?: string;
 63    address?: string;
 64    aToken?: boolean;
 65    priceInUsd?: string;
 66    decimals?: number;
 67  }
 68  
 69  export interface AssetInputProps<T extends Asset = Asset> {
 70    value: string;
 71    usdValue: string;
 72    symbol: string;
 73    onChange?: (value: string) => void;
 74    disabled?: boolean;
 75    disableInput?: boolean;
 76    onSelect?: (asset: T) => void;
 77    assets: T[];
 78    capType?: CapType;
 79    maxValue?: string;
 80    isMaxSelected?: boolean;
 81    inputTitle?: ReactNode;
 82    balanceText?: ReactNode;
 83    loading?: boolean;
 84    event?: TrackEventProps;
 85    selectOptionHeader?: ReactNode;
 86    selectOption?: (asset: T) => ReactNode;
 87    sx?: BoxProps;
 88    exchangeRateComponent?: ReactNode;
 89  }
 90  
 91  export const AssetInput = <T extends Asset = Asset>({
 92    value,
 93    usdValue,
 94    symbol,
 95    onChange,
 96    disabled,
 97    disableInput,
 98    onSelect,
 99    assets,
100    capType,
101    maxValue,
102    isMaxSelected,
103    inputTitle,
104    balanceText,
105    loading = false,
106    event,
107    selectOptionHeader,
108    selectOption,
109    sx = {},
110    exchangeRateComponent,
111  }: AssetInputProps<T>) => {
112    const theme = useTheme();
113    const trackEvent = useRootStore((store) => store.trackEvent);
114    const handleSelect = (event: SelectChangeEvent) => {
115      const newAsset = assets.find((asset) => asset.symbol === event.target.value) as T;
116      onSelect && onSelect(newAsset);
117      onChange && onChange('');
118    };
119  
120    const asset =
121      assets.length === 1
122        ? assets[0]
123        : assets && (assets.find((asset) => asset.symbol === symbol) as T);
124  
125    return (
126      <Box {...sx}>
127        <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
128          <Typography color="text.secondary">
129            {inputTitle ? inputTitle : <Trans>Amount</Trans>}
130          </Typography>
131          {capType && <AvailableTooltip capType={capType} />}
132        </Box>
133  
134        <Box
135          sx={(theme) => ({
136            border: `1px solid ${theme.palette.divider}`,
137            borderRadius: '6px',
138            overflow: 'hidden',
139          })}
140        >
141          <Box sx={{ display: 'flex', alignItems: 'center', mb: 0.5, px: 3, py: 2 }}>
142            {loading ? (
143              <Box sx={{ flex: 1 }}>
144                <CircularProgress color="inherit" size="16px" />
145              </Box>
146            ) : (
147              <InputBase
148                sx={{ flex: 1 }}
149                placeholder="0.00"
150                disabled={disabled || disableInput}
151                value={value}
152                autoFocus
153                onChange={(e) => {
154                  if (!onChange) return;
155                  if (Number(e.target.value) > Number(maxValue)) {
156                    onChange('-1');
157                  } else {
158                    onChange(e.target.value);
159                  }
160                }}
161                inputProps={{
162                  'aria-label': 'amount input',
163                  style: {
164                    fontSize: '21px',
165                    lineHeight: '28,01px',
166                    padding: 0,
167                    height: '28px',
168                    textOverflow: 'ellipsis',
169                    whiteSpace: 'nowrap',
170                    overflow: 'hidden',
171                  },
172                }}
173                // eslint-disable-next-line
174                inputComponent={NumberFormatCustom as any}
175              />
176            )}
177            {value !== '' && !disableInput && (
178              <IconButton
179                sx={{
180                  minWidth: 0,
181                  p: 0,
182                  left: 8,
183                  zIndex: 1,
184                  color: 'text.muted',
185                  '&:hover': {
186                    color: 'text.secondary',
187                  },
188                }}
189                onClick={() => {
190                  onChange && onChange('');
191                }}
192                disabled={disabled}
193              >
194                <XCircleIcon height={16} />
195              </IconButton>
196            )}
197            {!onSelect || assets.length === 1 ? (
198              <Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
199                <TokenIcon
200                  aToken={asset.aToken}
201                  symbol={asset.iconSymbol || asset.symbol}
202                  sx={{ mr: 2, ml: 4 }}
203                />
204                <Typography variant="h3" sx={{ lineHeight: '28px' }} data-cy={'inputAsset'}>
205                  {symbol}
206                </Typography>
207              </Box>
208            ) : (
209              <FormControl>
210                <Select
211                  disabled={disabled}
212                  value={asset.symbol}
213                  onChange={handleSelect}
214                  variant="outlined"
215                  className="AssetInput__select"
216                  data-cy={'assetSelect'}
217                  MenuProps={{
218                    sx: {
219                      maxHeight: '240px',
220                      '.MuiPaper-root': {
221                        border: theme.palette.mode === 'dark' ? '1px solid #EBEBED1F' : 'unset',
222                        boxShadow: '0px 2px 10px 0px #0000001A',
223                      },
224                    },
225                  }}
226                  sx={{
227                    p: 0,
228                    '&.AssetInput__select .MuiOutlinedInput-input': {
229                      p: 0,
230                      backgroundColor: 'transparent',
231                      pr: '24px !important',
232                    },
233                    '&.AssetInput__select .MuiOutlinedInput-notchedOutline': { display: 'none' },
234                    '&.AssetInput__select .MuiSelect-icon': {
235                      color: 'text.primary',
236                      right: '0%',
237                    },
238                  }}
239                  renderValue={(symbol) => {
240                    const asset =
241                      assets.length === 1
242                        ? assets[0]
243                        : assets && (assets.find((asset) => asset.symbol === symbol) as T);
244                    return (
245                      <Box
246                        sx={{ display: 'flex', alignItems: 'center' }}
247                        data-cy={`assetsSelectedOption_${asset.symbol.toUpperCase()}`}
248                      >
249                        <TokenIcon
250                          symbol={asset.iconSymbol || asset.symbol}
251                          aToken={asset.aToken}
252                          sx={{ mr: 2, ml: 4 }}
253                        />
254                        <Typography variant="main16" color="text.primary">
255                          {symbol}
256                        </Typography>
257                      </Box>
258                    );
259                  }}
260                >
261                  {selectOptionHeader ? selectOptionHeader : undefined}
262                  {assets.map((asset) => (
263                    <MenuItem
264                      key={asset.symbol}
265                      value={asset.symbol}
266                      data-cy={`assetsSelectOption_${asset.symbol.toUpperCase()}`}
267                    >
268                      {selectOption ? (
269                        selectOption(asset)
270                      ) : (
271                        <>
272                          <TokenIcon
273                            aToken={asset.aToken}
274                            symbol={asset.iconSymbol || asset.symbol}
275                            sx={{ fontSize: '22px', mr: 1 }}
276                          />
277                          <ListItemText sx={{ mr: 6 }}>{asset.symbol}</ListItemText>
278                          {asset.balance && <FormattedNumber value={asset.balance} compact />}
279                        </>
280                      )}
281                    </MenuItem>
282                  ))}
283                </Select>
284              </FormControl>
285            )}
286          </Box>
287  
288          <Box sx={{ display: 'flex', alignItems: 'center', height: '16px', px: 3, py: 2, mb: 1 }}>
289            {loading ? (
290              <Box sx={{ flex: 1 }} />
291            ) : (
292              <FormattedNumber
293                value={isNaN(Number(usdValue)) ? 0 : Number(usdValue)}
294                compact
295                symbol="USD"
296                variant="secondary12"
297                color="text.muted"
298                symbolsColor="text.muted"
299                flexGrow={1}
300              />
301            )}
302  
303            {asset.balance && onChange && (
304              <>
305                <Typography component="div" variant="secondary12" color="text.secondary">
306                  {balanceText && balanceText !== '' ? balanceText : <Trans>Balance</Trans>}{' '}
307                  <FormattedNumber
308                    value={asset.balance}
309                    compact
310                    variant="secondary12"
311                    color="text.secondary"
312                    symbolsColor="text.disabled"
313                  />
314                </Typography>
315                {!disableInput && (
316                  <Button
317                    size="small"
318                    sx={{ minWidth: 0, ml: '7px', p: 0 }}
319                    onClick={() => {
320                      if (event) {
321                        trackEvent(event.eventName, { ...event.eventParams });
322                      }
323  
324                      onChange('-1');
325                    }}
326                    disabled={disabled || isMaxSelected}
327                  >
328                    <Trans>Max</Trans>
329                  </Button>
330                )}
331              </>
332            )}
333          </Box>
334          {exchangeRateComponent && (
335            <Box
336              sx={{
337                background: theme.palette.background.surface,
338                borderTop: `1px solid ${theme.palette.divider}`,
339                px: 3,
340                py: 2,
341              }}
342            >
343              {exchangeRateComponent}
344            </Box>
345          )}
346        </Box>
347      </Box>
348    );
349  };