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 }