/ src / layouts / AppHeader.tsx
AppHeader.tsx
  1  import {
  2    InformationCircleIcon,
  3    SparklesIcon,
  4    SwitchHorizontalIcon,
  5  } from '@heroicons/react/outline';
  6  import { Trans } from '@lingui/macro';
  7  import {
  8    Badge,
  9    Button,
 10    NoSsr,
 11    Slide,
 12    styled,
 13    SvgIcon,
 14    Typography,
 15    useMediaQuery,
 16    useScrollTrigger,
 17    useTheme,
 18  } from '@mui/material';
 19  import Box from '@mui/material/Box';
 20  import * as React from 'react';
 21  import { useEffect, useState } from 'react';
 22  import { AvatarSize } from 'src/components/Avatar';
 23  import { ContentWithTooltip } from 'src/components/ContentWithTooltip';
 24  import { UserDisplay } from 'src/components/UserDisplay';
 25  import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
 26  import { useModalContext } from 'src/hooks/useModal';
 27  import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
 28  import { useRootStore } from 'src/store/root';
 29  import { ENABLE_TESTNET, FORK_ENABLED } from 'src/utils/marketsAndNetworksConfig';
 30  import { useShallow } from 'zustand/shallow';
 31  
 32  import { Link } from '../components/primitives/Link';
 33  import { uiConfig } from '../uiConfig';
 34  import { NavItems } from './components/NavItems';
 35  import { MobileMenu } from './MobileMenu';
 36  import { SettingsMenu } from './SettingsMenu';
 37  
 38  interface Props {
 39    children: React.ReactElement;
 40  }
 41  
 42  const StyledBadge = styled(Badge)(({ theme }) => ({
 43    '& .MuiBadge-badge': {
 44      top: '2px',
 45      right: '2px',
 46      borderRadius: '20px',
 47      width: '10px',
 48      height: '10px',
 49      backgroundColor: `${theme.palette.secondary.main}`,
 50      color: `${theme.palette.secondary.main}`,
 51      '&::after': {
 52        position: 'absolute',
 53        top: 0,
 54        left: 0,
 55        width: '100%',
 56        height: '100%',
 57        borderRadius: '50%',
 58        animation: 'ripple 1.2s infinite ease-in-out',
 59        border: '1px solid currentColor',
 60        content: '""',
 61      },
 62    },
 63    '@keyframes ripple': {
 64      '0%': {
 65        transform: 'scale(.8)',
 66        opacity: 1,
 67      },
 68      '100%': {
 69        transform: 'scale(2.4)',
 70        opacity: 0,
 71      },
 72    },
 73  }));
 74  
 75  function HideOnScroll({ children }: Props) {
 76    const { breakpoints } = useTheme();
 77    const md = useMediaQuery(breakpoints.down('md'));
 78    const trigger = useScrollTrigger({ threshold: md ? 160 : 80 });
 79  
 80    return (
 81      <Slide appear={false} direction="down" in={!trigger}>
 82        {children}
 83      </Slide>
 84    );
 85  }
 86  
 87  const SWITCH_VISITED_KEY = 'switchVisited';
 88  
 89  export function AppHeader() {
 90    const { breakpoints } = useTheme();
 91    const md = useMediaQuery(breakpoints.down('md'));
 92    const sm = useMediaQuery(breakpoints.down('sm'));
 93    const smd = useMediaQuery('(max-width:1120px)');
 94  
 95    const [visitedSwitch, setVisitedSwitch] = useState(() => {
 96      if (typeof window === 'undefined') return true;
 97      return Boolean(localStorage.getItem(SWITCH_VISITED_KEY));
 98    });
 99  
100    const [mobileDrawerOpen, setMobileDrawerOpen, currentMarketData] = useRootStore(
101      useShallow((state) => [
102        state.mobileDrawerOpen,
103        state.setMobileDrawerOpen,
104        state.currentMarketData,
105      ])
106    );
107  
108    const { openSwitch, openBridge, openReadMode } = useModalContext();
109    const { readOnlyMode } = useWeb3Context();
110    const [walletWidgetOpen, setWalletWidgetOpen] = useState(false);
111    const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
112  
113    useEffect(() => {
114      if (mobileDrawerOpen && !md) {
115        setMobileDrawerOpen(false);
116      }
117      if (walletWidgetOpen) {
118        setWalletWidgetOpen(false);
119      }
120      // eslint-disable-next-line react-hooks/exhaustive-deps
121    }, [md]);
122  
123    const headerHeight = 48;
124  
125    const toggleMobileMenu = (state: boolean) => {
126      if (md) setMobileDrawerOpen(state);
127      setMobileMenuOpen(state);
128    };
129  
130    const disableTestnet = () => {
131      localStorage.setItem('testnetsEnabled', 'false');
132      // Set window.location to trigger a page reload when navigating to the the dashboard
133      window.location.href = '/';
134    };
135  
136    const disableFork = () => {
137      localStorage.setItem('testnetsEnabled', 'false');
138      localStorage.removeItem('forkEnabled');
139      localStorage.removeItem('forkBaseChainId');
140      localStorage.removeItem('forkNetworkId');
141      localStorage.removeItem('forkRPCUrl');
142      // Set window.location to trigger a page reload when navigating to the the dashboard
143      window.location.href = '/';
144    };
145  
146    const handleSwitchClick = () => {
147      localStorage.setItem(SWITCH_VISITED_KEY, 'true');
148      setVisitedSwitch(true);
149      openSwitch();
150    };
151  
152    const handleBridgeClick = () => {
153      openBridge();
154    };
155  
156    const testnetTooltip = (
157      <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'start', gap: 1 }}>
158        <Typography variant="subheader1">
159          <Trans>Testnet mode is ON</Trans>
160        </Typography>
161        <Typography variant="description">
162          <Trans>The app is running in testnet mode. Learn how it works in</Trans>{' '}
163          <Link
164            href="https://aave.com/faq"
165            style={{ fontSize: '14px', fontWeight: 400, textDecoration: 'underline' }}
166          >
167            FAQ.
168          </Link>
169        </Typography>
170        <Button variant="outlined" sx={{ mt: '12px' }} onClick={disableTestnet}>
171          <Trans>Disable testnet</Trans>
172        </Button>
173      </Box>
174    );
175  
176    const forkTooltip = (
177      <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'start', gap: 1 }}>
178        <Typography variant="subheader1">
179          <Trans>Fork mode is ON</Trans>
180        </Typography>
181        <Typography variant="description">
182          <Trans>The app is running in fork mode.</Trans>
183        </Typography>
184        <Button variant="outlined" sx={{ mt: '12px' }} onClick={disableFork}>
185          <Trans>Disable fork</Trans>
186        </Button>
187      </Box>
188    );
189  
190    return (
191      <HideOnScroll>
192        <Box
193          component="header"
194          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
195          // @ts-ignore
196          sx={(theme) => ({
197            height: headerHeight,
198            position: 'sticky',
199            top: 0,
200            transition: theme.transitions.create('top'),
201            zIndex: theme.zIndex.appBar,
202            bgcolor: theme.palette.background.header,
203            padding: {
204              xs: mobileMenuOpen || walletWidgetOpen ? '8px 20px' : '8px 8px 8px 20px',
205              xsm: '8px 20px',
206            },
207            display: 'flex',
208            alignItems: 'center',
209            flexDirection: 'space-between',
210            boxShadow: 'inset 0px -1px 0px rgba(242, 243, 247, 0.16)',
211          })}
212        >
213          <Box
214            component={Link}
215            href="/"
216            aria-label="Go to homepage"
217            sx={{
218              lineHeight: 0,
219              mr: 3,
220              transition: '0.3s ease all',
221              '&:hover': { opacity: 0.7 },
222            }}
223            onClick={() => setMobileMenuOpen(false)}
224          >
225            <img src={uiConfig.appLogo} alt="AAVE" width={72} height={20} />
226          </Box>
227          <Box sx={{ mr: sm ? 1 : 3 }}>
228            {ENABLE_TESTNET && (
229              <ContentWithTooltip tooltipContent={testnetTooltip} offset={[0, -4]} withoutHover>
230                <Button
231                  variant="surface"
232                  size="small"
233                  color="primary"
234                  sx={{
235                    backgroundColor: '#B6509E',
236                    '&:hover, &.Mui-focusVisible': { backgroundColor: 'rgba(182, 80, 158, 0.7)' },
237                  }}
238                >
239                  TESTNET
240                  <SvgIcon sx={{ marginLeft: '2px', fontSize: '16px' }}>
241                    <InformationCircleIcon />
242                  </SvgIcon>
243                </Button>
244              </ContentWithTooltip>
245            )}
246          </Box>
247          <Box sx={{ mr: sm ? 1 : 3 }}>
248            {FORK_ENABLED && currentMarketData?.isFork && (
249              <ContentWithTooltip tooltipContent={forkTooltip} offset={[0, -4]} withoutHover>
250                <Button
251                  variant="surface"
252                  size="small"
253                  color="primary"
254                  sx={{
255                    backgroundColor: '#B6509E',
256                    '&:hover, &.Mui-focusVisible': { backgroundColor: 'rgba(182, 80, 158, 0.7)' },
257                  }}
258                >
259                  FORK
260                  <SvgIcon sx={{ marginLeft: '2px', fontSize: '16px' }}>
261                    <InformationCircleIcon />
262                  </SvgIcon>
263                </Button>
264              </ContentWithTooltip>
265            )}
266          </Box>
267  
268          <Box sx={{ display: { xs: 'none', md: 'block' } }}>
269            <NavItems />
270          </Box>
271  
272          <Box sx={{ flexGrow: 1 }} />
273  
274          <NoSsr>
275            <StyledBadge
276              invisible={visitedSwitch}
277              variant="dot"
278              badgeContent=""
279              color="secondary"
280              sx={{ mr: 2 }}
281            >
282              <Button
283                onClick={handleBridgeClick}
284                variant="surface"
285                sx={{ p: '7px 8px', minWidth: 'unset', gap: 2, alignItems: 'center' }}
286              >
287                {!smd && (
288                  <Typography component="span" typography="subheader1">
289                    Bridge GHO
290                  </Typography>
291                )}
292                <SvgIcon fontSize="small">
293                  <SparklesIcon />
294                </SvgIcon>
295              </Button>
296            </StyledBadge>
297          </NoSsr>
298  
299          <NoSsr>
300            <StyledBadge
301              invisible={true}
302              variant="dot"
303              badgeContent=""
304              color="secondary"
305              sx={{ mr: 2 }}
306            >
307              <Button
308                onClick={handleSwitchClick}
309                variant="surface"
310                sx={{ p: '7px 8px', minWidth: 'unset', gap: 2, alignItems: 'center' }}
311                aria-label="Switch tool"
312              >
313                {!smd && (
314                  <Typography component="span" typography="subheader1">
315                    Switch tokens
316                  </Typography>
317                )}
318                <SvgIcon fontSize="small">
319                  <SwitchHorizontalIcon />
320                </SvgIcon>
321              </Button>
322            </StyledBadge>
323          </NoSsr>
324  
325          {readOnlyMode ? (
326            <Button
327              variant="surface"
328              onClick={() => {
329                openReadMode();
330              }}
331            >
332              <UserDisplay
333                avatarProps={{ size: AvatarSize.SM }}
334                oneLiner={true}
335                titleProps={{ variant: 'buttonM' }}
336              />
337            </Button>
338          ) : (
339            <ConnectWalletButton />
340          )}
341  
342          <Box sx={{ display: { xs: 'none', md: 'block' } }}>
343            <SettingsMenu />
344          </Box>
345  
346          {!walletWidgetOpen && (
347            <Box sx={{ display: { xs: 'flex', md: 'none' } }}>
348              <MobileMenu
349                open={mobileMenuOpen}
350                setOpen={toggleMobileMenu}
351                headerHeight={headerHeight}
352              />
353            </Box>
354          )}
355        </Box>
356      </HideOnScroll>
357    );
358  }