Skip to content

Commit dc78032

Browse files
authored
feat: add VIP feature (#5671)
1 parent 3f5eb8b commit dc78032

62 files changed

Lines changed: 1401 additions & 269 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/solid-experts-agree.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@venusprotocol/evm": minor
3+
---
4+
5+
add VIP feature

apps/evm/src/App/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const AppVersionChecker = safeLazyLoad(() => import('containers/AppVersionChecke
1818
const GaslessChecker = safeLazyLoad(() => import('containers/GaslessChecker'));
1919
const ResendPayingGasModal = safeLazyLoad(() => import('containers/ResendPayingGasModal'));
2020
const ImportPositionsModal = safeLazyLoad(() => import('containers/ImportPositionsModal'));
21+
const VipModal = safeLazyLoad(() => import('containers/VipModal'));
2122

2223
const App = () => (
2324
<>
@@ -58,6 +59,10 @@ const App = () => (
5859
<ImportPositionsModal />
5960
</Suspense>
6061

62+
<Suspense>
63+
<VipModal />
64+
</Suspense>
65+
6166
<ChainUpgradeHandler />
6267

6368
<SentryErrorInfo />
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createPortal } from 'react-dom';
2+
3+
import { cn } from '@venusprotocol/ui';
4+
import { BODY_PORTAL_ID } from 'constants/layout';
5+
6+
export interface BodyBackdropProps extends React.HTMLAttributes<HTMLDivElement> {}
7+
8+
export const BodyBackdrop: React.FC<BodyBackdropProps> = ({ className, ...otherProps }) => {
9+
const backdropPortalDom =
10+
typeof document === 'undefined'
11+
? undefined
12+
: document.getElementById(BODY_PORTAL_ID) || document.body;
13+
14+
if (!backdropPortalDom) {
15+
return undefined;
16+
}
17+
18+
return createPortal(
19+
<div className={cn('fixed inset-0 backdrop-blur-xs z-9999', className)} {...otherProps} />,
20+
backdropPortalDom,
21+
);
22+
};

apps/evm/src/components/Dropdown/index.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useState } from 'react';
33
import { cn } from '@venusprotocol/ui';
44
import { useBreakpointUp } from 'hooks/responsive';
55

6-
import { useTranslation } from 'libs/translations';
76
import { Modal } from '../Modal';
87
import type { DropdownProps } from './types';
98

@@ -23,9 +22,8 @@ export const Dropdown = ({
2322
menuPosition = 'left',
2423
triggerOnHover = false,
2524
}: DropdownProps) => {
26-
const { t } = useTranslation();
27-
const [isDropdownOpened, setIsDropdownOpened] = useState(false);
28-
const handleToggleDropdown = () => setIsDropdownOpened(!isDropdownOpened);
25+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
26+
const handleToggleDropdown = () => setIsDropdownOpen(!isDropdownOpen);
2927

3028
const isMdOrUp = useBreakpointUp('md');
3129

@@ -40,28 +38,28 @@ export const Dropdown = ({
4038

4139
<div
4240
className="relative w-full"
43-
onMouseEnter={triggerOnHover ? () => setIsDropdownOpened(true) : undefined}
44-
onMouseLeave={triggerOnHover ? () => setIsDropdownOpened(false) : undefined}
41+
onMouseEnter={triggerOnHover && isMdOrUp ? () => setIsDropdownOpen(true) : undefined}
42+
onMouseLeave={triggerOnHover && isMdOrUp ? () => setIsDropdownOpen(false) : undefined}
4543
>
4644
{/* MD and up backdrop — click mode only */}
47-
{isDropdownOpened && !triggerOnHover && (
45+
{isDropdownOpen && !triggerOnHover && (
4846
<div
4947
className="fixed bottom-0 left-0 right-0 top-0 hidden md:block z-50"
50-
onClick={() => setIsDropdownOpened(false)}
48+
onClick={() => setIsDropdownOpen(false)}
5149
/>
5250
)}
5351

54-
{children({ isDropdownOpened, handleToggleDropdown })}
52+
{children({ isDropdownOpen, handleToggleDropdown })}
5553

5654
{/* XS to MD menu */}
57-
{isDropdownOpened && (
55+
{isDropdownOpen && (
5856
<div className="relative z-50 hidden min-w-full md:block">
5957
<div
6058
className={cn('pt-2 absolute min-w-full', menuPosition === 'right' && 'right-0')}
6159
>
6260
<div
6361
className={cn(
64-
'border-lightGrey bg-cards overflow-hidden border shadow',
62+
'border-lightGrey bg-cards overflow-visible border shadow',
6563
variant === 'quaternary' ? 'rounded-xl' : 'rounded-lg',
6664
menuClassName,
6765
)}
@@ -77,7 +75,7 @@ export const Dropdown = ({
7775
</div>
7876
)}
7977

80-
{optionsDom({ setIsDropdownOpened, optionClassName })}
78+
{optionsDom({ setIsDropdownOpen, optionClassName })}
8179
</div>
8280
</div>
8381
</div>
@@ -87,13 +85,13 @@ export const Dropdown = ({
8785

8886
{/* XS to MD menu */}
8987
<Modal
90-
isOpen={isDropdownOpened && !isMdOrUp}
88+
isOpen={isDropdownOpen && !isMdOrUp}
9189
handleClose={handleToggleDropdown}
9290
noHorizontalPadding
9391
onBlur={onBlur}
94-
title={menuTitle || t('select.defaultLabel')}
92+
title={menuTitle}
9593
>
96-
{optionsDom({ setIsDropdownOpened, optionClassName })}
94+
{optionsDom({ setIsDropdownOpen, optionClassName })}
9795
</Modal>
9896
</>
9997
);

apps/evm/src/components/Dropdown/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import type { FocusEventHandler } from 'react';
22

33
export interface DropdownProps {
44
children: ({
5-
isDropdownOpened,
5+
isDropdownOpen,
66
handleToggleDropdown,
7-
}: { isDropdownOpened: boolean; handleToggleDropdown: () => void }) => React.ReactNode;
7+
}: { isDropdownOpen: boolean; handleToggleDropdown: () => void }) => React.ReactNode;
88
onBlur?: FocusEventHandler<HTMLDivElement>;
99
optionClassName?: string;
1010
optionsDom: (props: {
11-
setIsDropdownOpened: (v: boolean) => void;
11+
setIsDropdownOpen: (v: boolean) => void;
1212
optionClassName?: string;
1313
}) => React.ReactElement;
1414
name?: string;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { SVGProps } from 'react';
2+
3+
const SvgConnect = (props: SVGProps<SVGSVGElement>) => (
4+
<svg
5+
width="20"
6+
height="20"
7+
viewBox="0 0 20 20"
8+
fill="none"
9+
xmlns="http://www.w3.org/2000/svg"
10+
{...props}
11+
>
12+
<path d="M10 2.5V9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
13+
14+
<path
15+
d="M6.60849 4.5C4.4565 5.68277 3 7.95624 3 10.5667C3 14.3958 6.13401 17.5 10 17.5C13.866 17.5 17 14.3958 17 10.5667C17 7.95624 15.5435 5.68277 13.3915 4.5"
16+
stroke="currentColor"
17+
strokeWidth="1.5"
18+
strokeLinecap="round"
19+
/>
20+
</svg>
21+
);
22+
23+
export default SvgConnect;

apps/evm/src/components/Icon/icons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,4 @@ export { default as graduationCap } from './graduationCap';
105105
export { default as medal } from './medal';
106106
export { default as trophy } from './trophy';
107107
export { default as swap } from './swap';
108+
export { default as connect } from './connect';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { fireEvent, screen } from '@testing-library/react';
2+
3+
import { BODY_PORTAL_ID } from 'constants/layout';
4+
import { renderComponent } from 'testUtils/render';
5+
6+
import { Modal } from '..';
7+
8+
describe('components/Modal', () => {
9+
beforeEach(() => {
10+
const backdropPortal = document.createElement('div');
11+
backdropPortal.id = BODY_PORTAL_ID;
12+
document.body.appendChild(backdropPortal);
13+
});
14+
15+
afterEach(() => {
16+
document.getElementById(BODY_PORTAL_ID)?.remove();
17+
});
18+
19+
it('renders the body backdrop into the shared portal and closes when the backdrop is clicked', () => {
20+
const handleCloseMock = vi.fn();
21+
22+
renderComponent(
23+
<Modal isOpen handleClose={handleCloseMock} title="Modal title">
24+
<div>Modal content</div>
25+
</Modal>,
26+
);
27+
28+
const backdropPortal = document.getElementById(BODY_PORTAL_ID);
29+
expect(backdropPortal).not.toBeNull();
30+
31+
const backdrop = backdropPortal!.firstElementChild;
32+
const modalContent = screen.getByText('Modal content');
33+
const modalRoot = modalContent.closest('[role="presentation"]');
34+
const dialog = modalContent.parentElement;
35+
36+
expect(backdrop).toBeInstanceOf(HTMLDivElement);
37+
expect(backdropPortal!.childElementCount).toBe(1);
38+
expect(backdropPortal).toContainElement(backdrop as HTMLDivElement);
39+
expect(modalContent).toBeInTheDocument();
40+
expect(modalRoot).toContainElement(dialog);
41+
expect(backdropPortal).not.toContainElement(modalContent);
42+
expect(dialog).toBeInstanceOf(HTMLDivElement);
43+
44+
fireEvent.click(backdrop!);
45+
46+
expect(handleCloseMock).toHaveBeenCalledTimes(1);
47+
});
48+
});

apps/evm/src/components/Modal/index.tsx

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import {
44
Modal as MUIModal,
55
type ModalProps as MUIModalProps,
66
} from '@mui/material';
7-
import Backdrop from '@mui/material/Backdrop';
87
import Fade from '@mui/material/Fade';
98
import type { ReactElement } from 'react';
109

1110
import config from 'config';
1211

1312
import { cn } from '@venusprotocol/ui';
13+
import { BodyBackdrop } from '../BodyBackdrop';
1414
import { Icon } from '../Icon';
1515
import { useModalStyles } from './styles';
1616

@@ -29,6 +29,7 @@ export const Modal: React.FC<ModalProps> = ({
2929
className,
3030
buttonClassName,
3131
children,
32+
componentsProps,
3233
handleClose,
3334
handleBackAction,
3435
isOpen,
@@ -39,56 +40,71 @@ export const Modal: React.FC<ModalProps> = ({
3940
}) => {
4041
const s = useModalStyles({ hasTitleComponent: Boolean(title), noHorizontalPadding });
4142

43+
const hasHeader = !!title || handleBackAction || handleClose;
44+
4245
const dom = (
4346
<div
4447
className={cn(
45-
'flex flex-col overflow-auto outline-hidden bg-dark-blue rounded-xl border border-blue absolute top-[50%] left-[50%] translate-y-[-50%] translate-x-[-50%] max-w-136 w-[calc(100%-2rem)] max-h-[calc(100%-2rem)]',
48+
'pointer-events-auto flex flex-col overflow-auto outline-hidden bg-dark-blue rounded-xl border border-blue absolute top-[50%] left-[50%] translate-y-[-50%] translate-x-[-50%] max-w-136 w-[calc(100%-2rem)] max-h-[calc(100%-2rem)]',
49+
!hasHeader && 'pt-4 md:pt-6',
4650
className,
4751
)}
4852
>
49-
<div css={s.titleWrapper}>
50-
{!!handleBackAction && (
51-
<Button
52-
css={s.backAction}
53-
className={buttonClassName}
54-
disableRipple
55-
onClick={handleBackAction}
56-
>
57-
<Icon css={s.backArrow} name="arrowRight" />
58-
</Button>
59-
)}
60-
<div css={s.titleComponent}>{title}</div>
53+
{hasHeader && (
54+
<div css={s.titleWrapper}>
55+
{!!handleBackAction && (
56+
<Button
57+
css={s.backAction}
58+
className={buttonClassName}
59+
disableRipple
60+
onClick={handleBackAction}
61+
>
62+
<Icon css={s.backArrow} name="arrowRight" />
63+
</Button>
64+
)}
65+
<div css={s.titleComponent}>{title}</div>
6166

62-
{handleClose && (
63-
<Button
64-
css={s.closeIcon}
65-
className={cn('right-6', buttonClassName)}
66-
disableRipple
67-
onClick={handleClose}
68-
>
69-
<Icon name="close" className="size-6" />
70-
</Button>
71-
)}
72-
</div>
67+
{handleClose && (
68+
<Button
69+
css={s.closeIcon}
70+
className={cn('right-6', buttonClassName)}
71+
disableRipple
72+
onClick={handleClose}
73+
>
74+
<Icon name="close" className="size-6" />
75+
</Button>
76+
)}
77+
</div>
78+
)}
7379

7480
<div css={s.contentWrapper}>{children as React.ReactNode}</div>
7581
</div>
7682
);
7783

7884
return (
79-
<MUIModal
80-
open={isOpen}
81-
onClose={handleClose}
82-
onBackdropClick={handleClose}
83-
closeAfterTransition
84-
BackdropComponent={Backdrop}
85-
BackdropProps={{
86-
timeout: 500,
87-
}}
88-
disablePortal={config.environment === 'storybook'}
89-
{...otherModalProps}
90-
>
91-
<Fade in={isOpen}>{dom}</Fade>
92-
</MUIModal>
85+
<>
86+
{isOpen && <BodyBackdrop onClick={handleClose} />}
87+
88+
<MUIModal
89+
open={isOpen}
90+
onClose={handleClose}
91+
closeAfterTransition
92+
hideBackdrop
93+
disablePortal={config.environment === 'storybook'}
94+
componentsProps={{
95+
...componentsProps,
96+
root: {
97+
...(componentsProps?.root || {}),
98+
className: cn('pointer-events-none', componentsProps?.root?.className),
99+
style: {
100+
zIndex: 99999,
101+
},
102+
},
103+
}}
104+
{...otherModalProps}
105+
>
106+
<Fade in={isOpen}>{dom}</Fade>
107+
</MUIModal>
108+
</>
93109
);
94110
};

0 commit comments

Comments
 (0)