Initial commit

This commit is contained in:
James Jia - Test
2023-09-08 13:52:13 -07:00
commit 4b86068d8f
584 changed files with 59963 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import { useDispatch, useSelector } from 'react-redux';
import { DialogTypes } from '@/constants/dialogs';
import { closeDialog, openDialog } from '@/state/dialogs';
import { getActiveDialog } from '@/state/dialogsSelectors';
import { ClosePositionDialog } from '@/views/dialogs/ClosePositionDialog';
import { DepositDialog } from '@/views/dialogs/DepositDialog';
import { DisconnectDialog } from '@/views/dialogs/DisconnectDialog';
import { ExchangeOfflineDialog } from '@/views/dialogs/ExchangeOfflineDialog';
import { HelpDialog } from '@/views/dialogs/HelpDialog';
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog';
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
export const DialogManager = () => {
const dispatch = useDispatch();
const activeDialog = useSelector(getActiveDialog);
if (!activeDialog) return null;
const { dialogProps, type } = activeDialog;
const modalProps = {
...dialogProps,
setIsOpen: (isOpen: boolean) => {
dispatch(
isOpen
? openDialog({ type: activeDialog.type, dialogProps: activeDialog.dialogProps })
: closeDialog()
);
},
};
return {
[DialogTypes.ClosePosition]: <ClosePositionDialog {...modalProps} />,
[DialogTypes.Deposit]: <DepositDialog {...modalProps} />,
[DialogTypes.DisconnectWallet]: <DisconnectDialog {...modalProps} />,
[DialogTypes.ExchangeOffline]: <ExchangeOfflineDialog {...modalProps} />,
[DialogTypes.FillDetails]: <FillDetailsDialog {...modalProps} />,
[DialogTypes.Help]: <HelpDialog {...modalProps} />,
[DialogTypes.MnemonicExport]: <MnemonicExportDialog {...modalProps} />,
[DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />,
[DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />,
[DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />,
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
}[type];
};
+115
View File
@@ -0,0 +1,115 @@
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { AbacusApiStatus } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { useApiState, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { Details } from '@/components/Details';
import { Output, OutputType } from '@/components/Output';
import { WithTooltip } from '@/components/WithTooltip';
enum FooterItems {
ChainHeight,
IndexerHeight,
}
enum ExchangeStatus {
Operational = 'Operational',
Degraded = 'Degraded',
}
export const FooterDesktop = () => {
const stringGetter = useStringGetter();
const { height, indexerHeight, status, statusErrorMessage } = useApiState();
const { exchangeStatus, label } =
!status || status === AbacusApiStatus.NORMAL
? {
exchangeStatus: ExchangeStatus.Operational,
label: stringGetter({ key: STRING_KEYS.OPERATIONAL }),
}
: {
exchangeStatus: ExchangeStatus.Degraded,
label: stringGetter({ key: STRING_KEYS.DEGRADED }),
};
return (
<Styled.Footer>
<WithTooltip
slotTooltip={
statusErrorMessage && (
<dl>
<dd>{statusErrorMessage}</dd>
</dl>
)
}
>
<Styled.Row>
<Styled.StatusDot exchangeStatus={exchangeStatus} />
<span>{label}</span>
</Styled.Row>
</WithTooltip>
{import.meta.env.MODE !== 'production' && (
<Styled.Details
withSeparators
items={[
{
key: FooterItems.ChainHeight,
label: 'Block Height',
value: <Output useGrouping type={OutputType.Number} value={height} />,
},
height !== indexerHeight && {
key: FooterItems.IndexerHeight,
label: 'Indexer Block Height',
value: (
<Styled.WarningOutput useGrouping type={OutputType.Number} value={indexerHeight} />
),
},
].filter(Boolean)}
layout="row"
/>
)}
</Styled.Footer>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Footer = styled.footer`
${layoutMixins.stickyFooter}
${layoutMixins.spacedRow}
grid-area: Footer;
font-size: 0.66em;
`;
Styled.Row = styled.div`
${layoutMixins.row}
padding: 0 1rem;
gap: 0.5rem;
`;
Styled.StatusDot = styled.div<{ exchangeStatus: ExchangeStatus }>`
width: 1em;
height: 1em;
border-radius: 50%;
background-color: ${({ exchangeStatus }) =>
({
[ExchangeStatus.Degraded]: css`var(--color-warning)`,
[ExchangeStatus.Operational]: css`var(--color-positive)`,
}[exchangeStatus])};
`;
Styled.WarningOutput = styled(Output)`
color: var(--color-warning);
`;
Styled.Details = styled(Details)`
${layoutMixins.scrollArea}
padding: 0 0.5em;
`;
+170
View File
@@ -0,0 +1,170 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { AppRoute } from '@/constants/routes';
import { useShouldShowFooter, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { NavigationMenu } from '@/components/NavigationMenu';
import { Icon } from '@/components/Icon';
import { BellIcon, MarketsIcon, PlayIcon, PortfolioIcon, ProfileIcon, TradeIcon } from '@/icons';
import { IconButton } from '@/components/IconButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { openDialog } from '@/state/dialogs';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
export const FooterMobile = () => {
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const canAccountTrade = useSelector(calculateCanAccountTrade);
const marketId = useSelector(getCurrentMarketId);
if (!useShouldShowFooter()) return null;
return (
<Styled.MobileNav>
<Styled.NavigationMenu
items={[
{
group: 'navigation',
items: [
canAccountTrade
? {
value: 'trade',
label: stringGetter({ key: STRING_KEYS.TRADE }),
slotBefore: (
<Styled.IconButton
iconComponent={TradeIcon}
size={ButtonSize.Large}
action={ButtonAction.Primary}
/>
),
href: `${AppRoute.Trade}/${marketId ?? DEFAULT_MARKETID}`,
}
: {
value: 'onboarding',
label: stringGetter({ key: STRING_KEYS.ONBOARDING }),
slotBefore: (
<Styled.IconButton
iconComponent={PlayIcon}
size={ButtonSize.Large}
action={ButtonAction.Primary}
/>
),
onClick: () => dispatch(openDialog({ type: DialogTypes.Onboarding })),
},
{
value: 'portfolio',
label: stringGetter({ key: STRING_KEYS.PORTFOLIO }),
slotBefore: <Styled.Icon iconComponent={PortfolioIcon} />,
href: AppRoute.Portfolio,
},
{
value: 'markets',
label: stringGetter({ key: STRING_KEYS.MARKETS }),
slotBefore: <Styled.Icon iconComponent={MarketsIcon} />,
href: AppRoute.Markets,
},
{
value: 'alerts',
label: stringGetter({ key: STRING_KEYS.ALERTS }),
slotBefore: <Styled.Icon iconComponent={BellIcon} />,
href: AppRoute.Alerts,
},
{
value: 'profile',
label: stringGetter({ key: STRING_KEYS.PROFILE }),
slotBefore: <Styled.Icon iconComponent={ProfileIcon} />,
href: AppRoute.Profile,
},
],
},
]}
orientation="horizontal"
itemOrientation="vertical"
/>
</Styled.MobileNav>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.MobileNav = styled.footer`
grid-area: Footer;
${layoutMixins.stickyFooter}
`;
Styled.NavigationMenu = styled(NavigationMenu)`
--navigationMenu-height: var(--page-currentFooterHeight);
--navigationMenu-item-height: var(--page-currentFooterHeight);
--navigationMenu-item-radius: 0;
--navigationMenu-item-checked-backgroundColor: transparent;
--navigationMenu-item-highlighted-backgroundColor: transparent;
--navigationMenu-item-highlighted-textColor: var(--color-text-2);
--navigationMenu-item-radius: 0px;
--navigationMenu-item-padding: 0px;
font: var(--font-tiny-book);
> section {
flex: 1;
ul[data-orientation='horizontal'] {
gap: 0;
> li {
flex: 1;
&[data-item='onboarding'],
&[data-item='trade'] {
span {
content-visibility: hidden;
position: absolute;
@supports not (content-visibility: hidden) {
opacity: 0;
width: 0;
overflow: hidden;
}
}
+ *,
+ * + * {
order: -1;
}
}
}
> li:not(:last-child):after {
content: '';
position: absolute;
width: var(--border-width);
top: 12.5%;
bottom: 12.5%;
right: 0;
background: linear-gradient(to bottom, transparent, var(--border-color), transparent);
}
}
}
`;
Styled.IconButton = styled(IconButton)`
margin-top: -0.25rem;
`;
Styled.Icon = styled(Icon)`
font-size: 1.5rem;
`;
+216
View File
@@ -0,0 +1,216 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { ButtonShape } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { AppRoute } from '@/constants/routes';
import { useStringGetter } from '@/hooks';
import { LogoShortIcon } from '@/icons';
import { Icon, IconName } from '@/components/Icon';
import { BellIcon } from '@/icons';
import { IconButton } from '@/components/IconButton';
import { NavigationMenu } from '@/components/NavigationMenu';
import { VerticalSeparator } from '@/components/Separator';
import { AccountMenu } from '@/views/menus/AccountMenu';
import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
import { NotificationsMenu } from '@/views/menus/NotificationsMenu';
import { LanguageSelector } from '@/views/menus/LanguageSelector';
import { openDialog } from '@/state/dialogs';
import { headerMixins } from '@/styles/headerMixins';
import { layoutMixins } from '@/styles/layoutMixins';
export const HeaderDesktop = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const navItems = [
{
group: 'navigation',
items: [
{
value: 'MARKET',
label: stringGetter({ key: STRING_KEYS.MARKET }),
href: AppRoute.Markets,
},
{
value: 'TRADE',
label: stringGetter({ key: STRING_KEYS.TRADE }),
href: AppRoute.Trade,
},
{
value: 'PORTFOLIO',
label: stringGetter({ key: STRING_KEYS.PORTFOLIO }),
href: AppRoute.Portfolio,
},
{
value: 'MORE',
label: stringGetter({ key: STRING_KEYS.MORE }),
subitems: [
{
value: 'DOCUMENTATION',
slotBefore: <Icon iconName={IconName.Terminal} />,
label: stringGetter({ key: STRING_KEYS.DOCUMENTATION }),
href: 'https://v4-teacher.vercel.app/',
},
{
value: 'MINTSCAN',
slotBefore: <Icon iconName={IconName.Mintscan} />,
label: stringGetter({ key: STRING_KEYS.MINTSCAN }),
href: 'https://testnet.mintscan.io/dydx-testnet',
},
{
value: 'COMMUNITY',
slotBefore: <Icon iconName={IconName.Discord} />,
label: stringGetter({ key: STRING_KEYS.COMMUNITY }),
href: 'https://discord.gg/dydx',
},
{
value: 'TERMS_OF_USE',
slotBefore: <Icon iconName={IconName.File} />,
label: stringGetter({ key: STRING_KEYS.TERMS_OF_USE }),
href: 'https://dydx.exchange/terms',
},
{
value: 'PRIVACY_POLICY',
slotBefore: <Icon iconName={IconName.Privacy} />,
label: stringGetter({ key: STRING_KEYS.PRIVACY_POLICY }),
href: 'https://dydx.exchange/privacy',
},
{
value: 'HELP',
slotBefore: <Icon iconName={IconName.HelpCircle} />,
label: stringGetter({ key: STRING_KEYS.HELP }),
onClick: (e: MouseEvent) => {
e.preventDefault();
dispatch(openDialog({ type: DialogTypes.Help }));
},
},
],
},
],
},
];
return (
<Styled.Header>
<Styled.LogoLink to="/">
<LogoShortIcon />
</Styled.LogoLink>
<VerticalSeparator />
<Styled.NavBefore>
<NetworkSelectMenu sideOffset={16} />
<VerticalSeparator />
<LanguageSelector sideOffset={16} />
</Styled.NavBefore>
<VerticalSeparator />
<Styled.NavigationMenu items={navItems} orientation="horizontal" />
<div role="separator" />
<Styled.NavAfter>
<Styled.IconButton
shape={ButtonShape.Rectangle}
iconName={IconName.HelpCircle}
onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))}
/>
<VerticalSeparator />
<NotificationsMenu
slotTrigger={<Styled.IconButton shape={ButtonShape.Rectangle} iconComponent={BellIcon} />}
/>
<VerticalSeparator />
<AccountMenu />
</Styled.NavAfter>
</Styled.Header>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Header = styled.header`
--header-horizontal-padding-mobile: 0.5rem;
--logo-width: 3.5rem;
${layoutMixins.container}
${layoutMixins.stickyHeader}
${layoutMixins.scrollSnapItem}
backdrop-filter: none;
grid-area: Header;
display: grid;
align-items: stretch;
grid-auto-flow: column;
grid-template:
'Logo . NavBefore . Nav . NavAfter' 100%
/ var(--logo-width) var(--border-width) calc(
var(--sidebar-width) - var(--logo-width) - var(--border-width)
)
var(--border-width) 1fr var(--border-width) auto;
font-size: 0.9375em;
:before {
backdrop-filter: blur(10px);
}
`;
Styled.NavigationMenu = styled(NavigationMenu)`
& {
--navigationMenu-height: var(--stickyArea-topHeight);
}
${layoutMixins.scrollArea}
padding: 0 0.5rem;
scroll-padding: 0 0.5rem;
`;
Styled.NavBefore = styled.div`
${layoutMixins.flexEqualColumns}
> * {
align-self: center;
margin: 0 0.4rem;
}
`;
Styled.LogoLink = styled(Link)`
display: flex;
align-self: stretch;
> svg {
margin: auto;
width: auto;
height: 45%;
}
`;
Styled.NavAfter = styled.div`
${layoutMixins.row}
justify-self: end;
padding-right: 0.75rem;
gap: 0.5rem;
a {
color: var(--color-text-1);
}
`;
Styled.IconButton = styled(IconButton)`
${headerMixins.button}
--button-border: none;
`;
+76
View File
@@ -0,0 +1,76 @@
import { NotificationStatus } from '@/constants/notifications';
import { ButtonSize } from '@/constants/buttons';
import { useNotifications } from '@/hooks/useNotifications';
import { useBreakpoints } from '@/hooks/useBreakpoints';
import { ToastArea } from '@/components/ToastArea';
import { Toast } from '@/components/Toast';
import { Button } from '@/components/Button';
import styled from 'styled-components';
type StyleProps = {
className?: string;
};
export const NotificationsToastArea = ({ className }: StyleProps) => {
const { notifications, getKey, getDisplayData, markUnseen, markSeen, isMenuOpen, onNotificationAction } = useNotifications();
const { isMobile } = useBreakpoints();
return (
<$ToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{Object.values(notifications)
// Sort by time of first trigger
.sort(
(n1, n2) =>
n1.timestamps[NotificationStatus.Triggered]! -
n2.timestamps[NotificationStatus.Triggered]!
)
.map((notification) => ({
notification,
key: getKey(notification),
displayData: getDisplayData(notification),
}))
.filter(({ displayData }) => displayData)
.map(({ notification, key, displayData }) => (
<Toast
key={key}
isOpen={notification.status < NotificationStatus.Unseen}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.description}
slotCustomContent={displayData.customContent}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
</Button>
}
actionDescription={displayData.actionDescription}
actionAltText={displayData.actionAltText}
duration={isMenuOpen ? Infinity : displayData.toastDuration}
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen, isClosedFromTimeout) => {
if (!isOpen)
// Toast timer expired without user interaction
if (isClosedFromTimeout) markUnseen(notification);
// Toast interacted with or dismissed
else markSeen(notification);
}}
lastUpdated={notification.timestamps[notification.status]}
/>
))}
</$ToastArea>
);
};
const $ToastArea = styled(ToastArea)`
position: absolute;
width: min(16rem, 100%);
inset: 0 0 0 auto;
padding: 0.75rem 0.75rem 0.75rem 0;
mask-image: linear-gradient(to left, transparent, white 0.5rem);
`;