diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 7cf136da0..11262f55e 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -39,6 +39,7 @@ export type GeoComplianceDialogProps = {}; export type GlobalCommandDialogProps = {}; export type HelpDialogProps = {}; export type ExternalNavKeplrDialogProps = {}; +export type LaunchMarketDialogProps = {}; export type ManageFundsDialogProps = { selectedTransferType?: string }; export type MnemonicExportDialogProps = {}; export type MobileDownloadDialogProps = { mobileAppUrl: string }; @@ -111,6 +112,7 @@ export const DialogTypes = unionize( GeoCompliance: ofType(), GlobalCommand: ofType(), Help: ofType(), + LaunchMarket: ofType(), ManageFunds: ofType(), MnemonicExport: ofType(), MobileDownload: ofType(), diff --git a/src/hooks/useCurrentMarketId.ts b/src/hooks/useCurrentMarketId.ts index 9f6ecfa95..580f1f8c1 100644 --- a/src/hooks/useCurrentMarketId.ts +++ b/src/hooks/useCurrentMarketId.ts @@ -9,6 +9,7 @@ import { LocalStorageKey } from '@/constants/localStorage'; import { DEFAULT_MARKETID, PREDICTION_MARKET } from '@/constants/markets'; import { AppRoute } from '@/constants/routes'; +import { useLaunchableMarkets } from '@/hooks/useLaunchableMarkets'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { getOpenPositions } from '@/state/accountSelectors'; @@ -31,6 +32,7 @@ export const useCurrentMarketId = () => { const marketIds = useAppSelector(getMarketIds, shallowEqual); const hasMarketIds = marketIds.length > 0; const activeTradeBoxDialog = useAppSelector(getActiveTradeBoxDialog); + const launchableMarkets = useLaunchableMarkets(); const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({ key: LocalStorageKey.LastViewedMarket, @@ -54,6 +56,13 @@ export const useCurrentMarketId = () => { return marketId ?? lastViewedMarket; }, [hasMarketIds, marketId]); + const isViewingUnlaunchedMarket = useMemo(() => { + if (launchableMarkets.data == null) return false; + return launchableMarkets.data.some((market) => { + return market.id === marketId; + }); + }, [marketId, launchableMarkets.data]); + useEffect(() => { // If v4_markets has not been subscribed to yet or marketId is not specified, default to validId if (!hasMarketIds || !marketId) { @@ -68,7 +77,7 @@ export const useCurrentMarketId = () => { } } else { // If v4_markets has been subscribed to, check if marketId is valid - if (!marketIds.includes(marketId)) { + if (!marketIds.includes(marketId) && !isViewingUnlaunchedMarket) { // If marketId is not valid (i.e. final settlement), navigate to markets page navigate(AppRoute.Markets, { replace: true, @@ -95,13 +104,20 @@ export const useCurrentMarketId = () => { dispatch(closeDialogInTradeBox()); } } - }, [hasMarketIds, marketId]); + }, [hasMarketIds, isViewingUnlaunchedMarket, marketId, navigate]); useEffect(() => { // Check for marketIds otherwise Abacus will silently fail its isMarketValid check - if (hasMarketIds) { + if (isViewingUnlaunchedMarket) { + abacusStateManager.setMarket(DEFAULT_MARKETID); + abacusStateManager.setTradeValue({ value: null, field: null }); + } else if (hasMarketIds) { abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID); abacusStateManager.setTradeValue({ value: null, field: null }); } - }, [selectedNetwork, hasMarketIds, marketId]); + }, [isViewingUnlaunchedMarket, selectedNetwork, hasMarketIds, marketId]); + + return { + isViewingUnlaunchedMarket, + }; }; diff --git a/src/hooks/useLaunchableMarkets.ts b/src/hooks/useLaunchableMarkets.ts index 820190ce5..c7574f5bd 100644 --- a/src/hooks/useLaunchableMarkets.ts +++ b/src/hooks/useLaunchableMarkets.ts @@ -6,6 +6,8 @@ import { shallowEqual } from 'react-redux'; import { useAppSelector } from '@/state/appTypes'; import { getMarketIds } from '@/state/perpetualsSelectors'; +import { getTickerFromMarketmapId } from '@/lib/assetUtils'; + import { useSelectedNetwork } from './useSelectedNetwork'; type MarketMapTicker = { @@ -46,6 +48,42 @@ export type MarketMapResponse = { export type HydratedMarketMap = MarketMapResponse['market_map']['markets'][string] & { id: string }; +const MOCK_MARKETMAP_DATA = { + chain_id: '1', + last_updated: '2021-10-12T00:00:00Z', + market_map: { + markets: { + 'BAG,raydium,D8r8XTuCrUhLheWeGXSwC3G92RhASficV3YA7B2XWcLv/USD': { + ticker: { + currency_pair: { + Base: 'BAG,raydium,D8r8XTuCrUhLheWeGXSwC3G92RhASficV3YA7B2XWcLv', + Quote: 'USD', + }, + decimals: '12', + min_provider_count: '1', + enabled: false, + metadata_JSON: + '{"reference_price":1767877746,"liquidity":205137,"aggregate_ids":[{"venue":"coinmarketcap","ID":"30088"}]}', + }, + provider_configs: [ + { + name: 'raydium_api', + off_chain_ticker: + 'BAG,raydium,D8r8XTuCrUhLheWeGXSwC3G92RhASficV3YA7B2XWcLv/SOL,raydium,So11111111111111111111111111111111111111112', + normalize_by_pair: { + Base: 'SOL', + Quote: 'USD', + }, + invert: false, + metadata_JSON: + '{"base_token_vault":{"token_vault_address":"7eLwyCqfhxKLsKeFwcN4JdfspKK22rSC4uQHNy3zWNPB","token_decimals":9},"quote_token_vault":{"token_vault_address":"Cr7Yo8Uf5f8pzMsY3ZwgDFNx85nb3UDvPfQxuWG4acxc","token_decimals":9},"amm_info_address":"Bv7mM5TwLxsukrRrwzEc6TFAj22GAdVCcH5ViAZFNZC","open_orders_address":"Du6ZaABu8cxmCAvwoGMixZgZuw57cCQc8xE8yRenaxL4"}', + }, + ], + }, + }, + }, +}; + export const useMarketMap = () => { const { selectedNetwork } = useSelectedNetwork(); @@ -67,14 +105,14 @@ export const useLaunchableMarkets = () => { const marketIds = useAppSelector(getMarketIds, shallowEqual); const filteredPotentialMarkets: HydratedMarketMap[] = useMemo(() => { - const marketMap = launchableMarkets.data?.market_map?.markets; + const marketMap = (launchableMarkets.data ?? MOCK_MARKETMAP_DATA)?.market_map?.markets; if (!marketMap) { return []; } return Object.entries(marketMap) .map(([id, data]) => ({ - id, + id: getTickerFromMarketmapId(id), ...data, })) .filter(({ id }) => { diff --git a/src/layout/DialogManager.tsx b/src/layout/DialogManager.tsx index 3e924f4cb..6cfef2a7e 100644 --- a/src/layout/DialogManager.tsx +++ b/src/layout/DialogManager.tsx @@ -20,6 +20,7 @@ import { ExternalNavStrideDialog } from '@/views/dialogs/ExternalNavStrideDialog import { GeoComplianceDialog } from '@/views/dialogs/GeoComplianceDialog'; import { GlobalCommandDialog } from '@/views/dialogs/GlobalCommandDialog'; import { HelpDialog } from '@/views/dialogs/HelpDialog'; +import { LaunchMarketDialog } from '@/views/dialogs/LaunchMarketDialog'; import { ManageFundsDialog } from '@/views/dialogs/ManageFundsDialog'; import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog'; import { MobileDownloadDialog } from '@/views/dialogs/MobileDownloadDialog'; @@ -80,6 +81,7 @@ export const DialogManager = () => { GlobalCommand: (args) => , Help: (args) => , ExternalNavKeplr: (args) => , + LaunchMarket: (args) => , ManageFunds: (args) => , MnemonicExport: (args) => , MobileDownload: (args) => , diff --git a/src/lib/__test__/assetUtils.spec.ts b/src/lib/__test__/assetUtils.spec.ts index a28e808cd..8bcca7b33 100644 --- a/src/lib/__test__/assetUtils.spec.ts +++ b/src/lib/__test__/assetUtils.spec.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { getDisplayableAssetFromBaseAsset, getDisplayableTickerFromMarket } from '../assetUtils'; +import { + getDisplayableAssetFromBaseAsset, + getDisplayableTickerFromMarket, + getTickerFromMarketmapId, +} from '../assetUtils'; const ASSET_WITH_DEX_AND_ADDRESS = 'BUFFI,uniswap_v3,0x4c1b1302220d7de5c22b495e78b72f2dd2457d45'; @@ -65,3 +69,23 @@ describe('getDisplayableTickerFromMarket', () => { expect(getDisplayableTickerFromMarket('ETH')).toEqual(''); }); }); + +describe('getTickerFromMarketmapId', () => { + it('should return a market ticker from a basic marketmap id', () => { + expect(getTickerFromMarketmapId('ETH/USD')).toEqual('ETH-USD'); + }); + + it('should return a market ticker from a marketmap id w/ dex', () => { + expect(getTickerFromMarketmapId(`${ASSET_WITH_DEX_AND_ADDRESS}/USD`)).toEqual( + `${ASSET_WITH_DEX_AND_ADDRESS}-USD` + ); + }); + + it('should handle invalid marketmap id', () => { + expect(getTickerFromMarketmapId('ETH')).toEqual('ETH'); + }); + + it('should handle empty marketmap id', () => { + expect(getTickerFromMarketmapId('')).toEqual(''); + }); +}); diff --git a/src/lib/assetUtils.ts b/src/lib/assetUtils.ts index d258bd17f..32037f1dd 100644 --- a/src/lib/assetUtils.ts +++ b/src/lib/assetUtils.ts @@ -27,3 +27,7 @@ export const getDisplayableTickerFromMarket = (market: string): string => { return `${base}-${quoteAsset}`; }; + +export const getTickerFromMarketmapId = (marketmapId: string): string => { + return marketmapId.replace('/', '-'); +}; diff --git a/src/pages/trade/InnerPanel.tsx b/src/pages/trade/InnerPanel.tsx index e6d041ec3..275a1faeb 100644 --- a/src/pages/trade/InnerPanel.tsx +++ b/src/pages/trade/InnerPanel.tsx @@ -16,6 +16,7 @@ import { useAppSelector } from '@/state/appTypes'; import { getSelectedLocale } from '@/state/localizationSelectors'; import abacusStateManager from '@/lib/abacus'; +import { isTruthy } from '@/lib/isTruthy'; enum Tab { Price = 'Price', @@ -24,7 +25,11 @@ enum Tab { Details = 'Details', } -export const InnerPanel = () => { +export const InnerPanel = ({ + isViewingUnlaunchedMarket, +}: { + isViewingUnlaunchedMarket?: boolean; +}) => { const stringGetter = useStringGetter(); const selectedLocale = useAppSelector(getSelectedLocale); @@ -41,7 +46,7 @@ export const InnerPanel = () => { label: stringGetter({ key: STRING_KEYS.PRICE_CHART_SHORT }), value: Tab.Price, }, - { + !isViewingUnlaunchedMarket && { content: ( { @@ -62,7 +67,7 @@ export const InnerPanel = () => { label: stringGetter({ key: STRING_KEYS.DEPTH_CHART_SHORT }), value: Tab.Depth, }, - { + !isViewingUnlaunchedMarket && { content: , label: stringGetter({ key: STRING_KEYS.FUNDING_RATE_CHART_SHORT }), value: Tab.Funding, @@ -72,7 +77,7 @@ export const InnerPanel = () => { label: stringGetter({ key: STRING_KEYS.DETAILS }), value: Tab.Details, }, - ]} + ].filter(isTruthy)} slotToolbar={} withTransitions={false} /> diff --git a/src/pages/trade/LaunchableMarket.tsx b/src/pages/trade/LaunchableMarket.tsx new file mode 100644 index 000000000..076b68458 --- /dev/null +++ b/src/pages/trade/LaunchableMarket.tsx @@ -0,0 +1,205 @@ +import { useMemo, useRef, useState } from 'react'; + +import { useMatch } from 'react-router-dom'; +import styled, { css } from 'styled-components'; + +import { TradeLayouts } from '@/constants/layout'; +import { AppRoute } from '@/constants/routes'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; + +import breakpoints from '@/styles/breakpoints'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { DetachedSection } from '@/components/ContentSection'; +import { AccountInfo } from '@/views/AccountInfo'; +import { LaunchMarketSidePanel } from '@/views/LaunchMarketSidePanel'; + +import { useAppSelector } from '@/state/appTypes'; +import { getSelectedTradeLayout } from '@/state/layoutSelectors'; + +import { getDisplayableTickerFromMarket } from '@/lib/assetUtils'; + +import { HorizontalPanel } from './HorizontalPanel'; +import { InnerPanel } from './InnerPanel'; +import { MarketSelectorAndStats } from './MarketSelectorAndStats'; +import { MobileBottomPanel } from './MobileBottomPanel'; +import { MobileTopPanel } from './MobileTopPanel'; +import { TradeHeaderMobile } from './TradeHeaderMobile'; + +const LaunchableMarket = () => { + const tradePageRef = useRef(null); + const { isTablet } = useBreakpoints(); + const tradeLayout = useAppSelector(getSelectedTradeLayout); + const match = useMatch(`/${AppRoute.Trade}/:marketId`); + const { marketId } = match?.params ?? {}; + + const displayableTicker = useMemo(() => { + return getDisplayableTickerFromMarket(marketId ?? ''); + }, [marketId]); + + const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true); + + return isTablet ? ( + <$TradeLayoutMobile> + + +
+ + + + + + + + + + + +
+ + ) : ( + <$TradeLayout + ref={tradePageRef} + tradeLayout={tradeLayout} + isHorizontalPanelOpen={isHorizontalPanelOpen} + > +
+ +
+ + <$GridSection gridArea="Side" tw="grid-rows-[auto_minmax(0,1fr)]"> + + <$LaunchMarketSidePanel launchableMarketId={displayableTicker} /> + + + <$GridSection gridArea="Inner"> + + + + <$GridSection gridArea="Horizontal"> + + + + ); +}; + +export default LaunchableMarket; + +const $TradeLayout = styled.article<{ + tradeLayout: TradeLayouts; + isHorizontalPanelOpen: boolean; +}>` + --horizontalPanel-height: 18rem; + + // Constants + /* prettier-ignore */ + --layout-default: + 'Top Top' auto + 'Side Inner' minmax(0, 1fr) + 'Side Horizontal' minmax(var(--tabs-height), var(--horizontalPanel-height)) + / var(--sidebar-width) 1fr; + + /* prettier-ignore */ + --layout-default-desktopMedium: + 'Side Top' auto + 'Side Inner' minmax(0, 1fr) + 'Side Horizontal' minmax(var(--tabs-height), var(--horizontalPanel-height)) + / var(--sidebar-width) 1fr; + + /* prettier-ignore */ + --layout-alternative: + 'Top Top' auto + 'Inner Side' minmax(0, 1fr) + 'Horizontal Side' minmax(var(--tabs-height), var(--horizontalPanel-height)) + / 1fr var(--sidebar-width); + + /* prettier-ignore */ + --layout-alternative-desktopMedium: + 'Top Side' auto + 'Inner Side' minmax(0, 1fr) + 'Horizontal Side' minmax(var(--tabs-height), var(--horizontalPanel-height)) + / 1fr var(--sidebar-width); + + // Props/defaults + + --layout: var(--layout-default); + + // Variants + @media ${breakpoints.desktopMedium} { + --layout: var(--layout-default-desktopMedium); + } + + ${({ tradeLayout }) => + ({ + [TradeLayouts.Default]: null, + [TradeLayouts.Alternative]: css` + --layout: var(--layout-alternative); + @media ${breakpoints.desktopMedium} { + --layout: var(--layout-alternative-desktopMedium); + } + `, + [TradeLayouts.Reverse]: css` + direction: rtl; + + > * { + direction: initial; + } + `, + })[tradeLayout]} + + ${({ isHorizontalPanelOpen }) => + !isHorizontalPanelOpen && + css` + --horizontalPanel-height: auto !important; + `} + + // Rules + width: 0; + min-width: 100%; + height: 0; + min-height: 100%; + + display: grid; + grid-template: var(--layout); + + ${layoutMixins.withOuterAndInnerBorders} + + @media (prefers-reduced-motion: no-preference) { + transition: grid-template 0.2s var(--ease-out-expo); + } + + > * { + display: grid; + } + + > section { + contain: strict; + } +`; + +const $TradeLayoutMobile = styled.article` + ${layoutMixins.contentContainerPage} + min-height: 100%; + + ${layoutMixins.stickyArea1} + --stickyArea1-topHeight: var(--page-header-height-mobile); + --stickyArea1-bottomHeight: var(--page-footer-height-mobile); + + ${layoutMixins.withInnerHorizontalBorders} + + > div:nth-child(2) { + flex: 1; + + ${layoutMixins.flexColumn} + justify-content: start; + } +`; + +const $GridSection = styled.section<{ gridArea: string }>` + grid-area: ${({ gridArea }) => gridArea}; +`; + +const $LaunchMarketSidePanel = styled(LaunchMarketSidePanel)` + border-top: var(--border-width) solid var(--color-border); +`; diff --git a/src/pages/trade/MarketSelectorAndStats.tsx b/src/pages/trade/MarketSelectorAndStats.tsx index 83221f3fc..c23ac217f 100644 --- a/src/pages/trade/MarketSelectorAndStats.tsx +++ b/src/pages/trade/MarketSelectorAndStats.tsx @@ -6,22 +6,33 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { VerticalSeparator } from '@/components/Separator'; import { MarketStatsDetails } from '@/views/MarketStatsDetails'; import { MarketsDropdown } from '@/views/MarketsDropdown'; +import { UnlaunchedMarketStatsDetails } from '@/views/UnlaunchedMarketStatsDetails'; import { useAppSelector } from '@/state/appTypes'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketDisplayId } from '@/state/perpetualsSelectors'; -export const MarketSelectorAndStats = ({ className }: { className?: string }) => { +export const MarketSelectorAndStats = ({ + className, + launchableMarketId, +}: { + className?: string; + launchableMarketId?: string; +}) => { const { id = '' } = useAppSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; const currentMarketId = useAppSelector(getCurrentMarketDisplayId) ?? ''; return ( <$Container className={className}> - + - + {launchableMarketId ? : } ); }; diff --git a/src/pages/trade/MobileBottomPanel.tsx b/src/pages/trade/MobileBottomPanel.tsx index fa890c3fd..03d643984 100644 --- a/src/pages/trade/MobileBottomPanel.tsx +++ b/src/pages/trade/MobileBottomPanel.tsx @@ -5,6 +5,7 @@ import { useStringGetter } from '@/hooks/useStringGetter'; import { MobileTabs } from '@/components/Tabs'; import { MarketDetails } from '@/views/MarketDetails'; import { MarketStatsDetails } from '@/views/MarketStatsDetails'; +import { UnlaunchedMarketStatsDetails } from '@/views/UnlaunchedMarketStatsDetails'; enum InfoSection { Statistics = 'Statistics', @@ -12,7 +13,11 @@ enum InfoSection { Vault = 'Vault', } -export const MobileBottomPanel = () => { +export const MobileBottomPanel = ({ + isViewingUnlaunchedMarket, +}: { + isViewingUnlaunchedMarket?: boolean; +}) => { const stringGetter = useStringGetter(); return ( @@ -22,7 +27,11 @@ export const MobileBottomPanel = () => { { value: InfoSection.Statistics, label: stringGetter({ key: STRING_KEYS.STATISTICS }), - content: , + content: isViewingUnlaunchedMarket ? ( + + ) : ( + + ), }, { value: InfoSection.About, diff --git a/src/pages/trade/MobileTopPanel.tsx b/src/pages/trade/MobileTopPanel.tsx index 832e36ec0..9917d2948 100644 --- a/src/pages/trade/MobileTopPanel.tsx +++ b/src/pages/trade/MobileTopPanel.tsx @@ -22,6 +22,8 @@ import { LiveTrades } from '@/views/tables/LiveTrades'; import { useAppSelector } from '@/state/appTypes'; import { getSelectedLocale } from '@/state/localizationSelectors'; +import { isTruthy } from '@/lib/isTruthy'; + enum Tab { Account = 'Account', Price = 'Price', @@ -41,7 +43,11 @@ const TabButton = ({ value, label, icon }: { value: Tab; label: string; icon: Ic ); -export const MobileTopPanel = () => { +export const MobileTopPanel = ({ + isViewingUnlaunchedMarket, +}: { + isViewingUnlaunchedMarket?: boolean; +}) => { const stringGetter = useStringGetter(); const selectedLocale = useAppSelector(getSelectedLocale); @@ -61,19 +67,19 @@ export const MobileTopPanel = () => { value: Tab.Price, icon: IconName.PriceChart, }, - { + !isViewingUnlaunchedMarket && { content: , label: stringGetter({ key: STRING_KEYS.DEPTH_CHART_SHORT }), value: Tab.Depth, icon: IconName.DepthChart, }, - { + !isViewingUnlaunchedMarket && { content: , label: stringGetter({ key: STRING_KEYS.FUNDING_RATE_CHART_SHORT }), value: Tab.Funding, icon: IconName.FundingChart, }, - { + !isViewingUnlaunchedMarket && { content: ( <$ScrollableTableContainer> @@ -83,7 +89,7 @@ export const MobileTopPanel = () => { value: Tab.OrderBook, icon: IconName.Orderbook, }, - { + !isViewingUnlaunchedMarket && { content: ( <$ScrollableTableContainer> @@ -93,7 +99,7 @@ export const MobileTopPanel = () => { value: Tab.LiveTrades, icon: IconName.Clock, }, - ]; + ].filter(isTruthy); return ( <$Tabs diff --git a/src/pages/trade/Trade.tsx b/src/pages/trade/Trade.tsx index fd580d836..63040117a 100644 --- a/src/pages/trade/Trade.tsx +++ b/src/pages/trade/Trade.tsx @@ -22,6 +22,7 @@ import { getSelectedTradeLayout } from '@/state/layoutSelectors'; import { HorizontalPanel } from './HorizontalPanel'; import { InnerPanel } from './InnerPanel'; +import LaunchableMarket from './LaunchableMarket'; import { MarketSelectorAndStats } from './MarketSelectorAndStats'; import { MobileBottomPanel } from './MobileBottomPanel'; import { MobileTopPanel } from './MobileTopPanel'; @@ -32,7 +33,7 @@ import { VerticalPanel } from './VerticalPanel'; const TradePage = () => { const tradePageRef = useRef(null); - useCurrentMarketId(); + const { isViewingUnlaunchedMarket } = useCurrentMarketId(); const { isTablet } = useBreakpoints(); const tradeLayout = useAppSelector(getSelectedTradeLayout); const canAccountTrade = useAppSelector(calculateCanAccountTrade); @@ -42,6 +43,10 @@ const TradePage = () => { usePageTitlePriceUpdates(); useTradeFormInputs(); + if (isViewingUnlaunchedMarket) { + return ; + } + return isTablet ? ( <$TradeLayoutMobile> diff --git a/src/pages/trade/TradeHeaderMobile.tsx b/src/pages/trade/TradeHeaderMobile.tsx index 6d0e5cc7c..135111365 100644 --- a/src/pages/trade/TradeHeaderMobile.tsx +++ b/src/pages/trade/TradeHeaderMobile.tsx @@ -15,23 +15,26 @@ import { useAppSelector } from '@/state/appTypes'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketData } from '@/state/perpetualsSelectors'; +import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; import { MustBigNumber } from '@/lib/numbers'; -export const TradeHeaderMobile = () => { +export const TradeHeaderMobile = ({ launchableMarketId }: { launchableMarketId?: string }) => { const { name, id } = useAppSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; const navigate = useNavigate(); const { displayId, priceChange24H, priceChange24HPercent } = useAppSelector(getCurrentMarketData, shallowEqual) ?? {}; + const baseAsset = launchableMarketId ? getDisplayableAssetFromBaseAsset(launchableMarketId) : id; + return ( <$Header> navigate(AppRoute.Markets)} />
- + <$Name>

{name}

- {displayId} + {launchableMarketId ?? displayId}
diff --git a/src/views/LaunchMarketSidePanel.tsx b/src/views/LaunchMarketSidePanel.tsx new file mode 100644 index 000000000..db93c7bb1 --- /dev/null +++ b/src/views/LaunchMarketSidePanel.tsx @@ -0,0 +1,91 @@ +import styled from 'styled-components'; + +import { ButtonAction } from '@/constants/buttons'; +import { DialogTypes } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { Button } from '@/components/Button'; +import { Output, OutputType } from '@/components/Output'; + +import { useAppDispatch } from '@/state/appTypes'; +import { openDialog } from '@/state/dialogs'; + +export const LaunchMarketSidePanel = ({ + className, + launchableMarketId, +}: { + className?: string; + launchableMarketId?: string; +}) => { + const dispatch = useAppDispatch(); + const stringGetter = useStringGetter(); + + const items = [ + { + title: stringGetter({ + key: STRING_KEYS.DEPOSIT_TO_DESTINATION, + params: { DESTINATION_CHAIN: 'MegaVault' }, + }), + body: stringGetter({ + key: STRING_KEYS.MARKET_LAUNCH_DETAILS_2, + params: { + DEPOSIT_AMOUNT: `${10_000} USDC`, + APR_PERCENTAGE: , + PAST_DAYS: 30, + }, + }), + }, + { + title: stringGetter({ key: STRING_KEYS.TRADE }), + body: stringGetter({ + key: STRING_KEYS.AVAILABLE_TO_TRADE_POST_LAUNCH, + params: { MARKET: launchableMarketId }, + }), + }, + ]; + + const steps = items.map((item, idx) => ( +
+
+ {idx + 1} +
+
+ {item.title} + {item.body} +
+
+ )); + + return ( + <$Container className={className}> +

+ {stringGetter({ + key: STRING_KEYS.INSTANTLY_LAUNCH, + params: { MARKET: launchableMarketId }, + })} +

+
{steps}
+ + + ); +}; + +const $Container = styled.section` + display: grid; + grid-template-rows: auto 1fr auto; + gap: 1rem; + padding: 1rem; + + button { + width: 100%; + } +`; diff --git a/src/views/MarketsDropdown.tsx b/src/views/MarketsDropdown.tsx index f3db1dc4f..41aae3aa0 100644 --- a/src/views/MarketsDropdown.tsx +++ b/src/views/MarketsDropdown.tsx @@ -256,14 +256,22 @@ const MarketsDropdownContent = ({ }; export const MarketsDropdown = memo( - ({ currentMarketId, symbol = '' }: { currentMarketId?: string; symbol: string | null }) => { + ({ + currentMarketId, + isViewingUnlaunchedMarket, + symbol = '', + }: { + currentMarketId?: string; + isViewingUnlaunchedMarket?: boolean; + symbol: string | null; + }) => { const [isOpen, setIsOpen] = useState(false); const stringGetter = useStringGetter(); const navigate = useNavigate(); const marketMaxLeverage = useParameterizedSelector(getMarketMaxLeverage, currentMarketId); const leverageTag = - currentMarketId != null ? ( + !isViewingUnlaunchedMarket && currentMarketId != null ? ( @@ -287,7 +295,16 @@ export const MarketsDropdown = memo( ) : (
-

{currentMarketId}

+ {isViewingUnlaunchedMarket ? ( +
+ Not Launched +

+ {currentMarketId} +

+
+ ) : ( +

{currentMarketId}

+ )} {leverageTag}
)} diff --git a/src/views/UnlaunchedMarketStatsDetails.tsx b/src/views/UnlaunchedMarketStatsDetails.tsx new file mode 100644 index 000000000..5952e9337 --- /dev/null +++ b/src/views/UnlaunchedMarketStatsDetails.tsx @@ -0,0 +1,132 @@ +import styled, { css } from 'styled-components'; + +import { STRING_KEYS } from '@/constants/localization'; +import { USD_DECIMALS } from '@/constants/numbers'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import { useStringGetter } from '@/hooks/useStringGetter'; + +import breakpoints from '@/styles/breakpoints'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Details } from '@/components/Details'; +import { Output, OutputType } from '@/components/Output'; +import { VerticalSeparator } from '@/components/Separator'; + +type ElementProps = { + showMidMarketPrice?: boolean; +}; + +enum MarketStats { + MARKET_CAP = 'MARKET_CAP', + SPOT_VOLUME_24H = 'SPOT_VOLUME_24H', +} + +const defaultMarketStatistics = Object.values(MarketStats); + +const DetailsItem = ({ value, stat }: { value: number | null | undefined; stat: MarketStats }) => { + switch (stat) { + case MarketStats.MARKET_CAP: { + return <$Output type={OutputType.Fiat} value={value} fractionDigits={USD_DECIMALS} />; + } + case MarketStats.SPOT_VOLUME_24H: { + return <$Output type={OutputType.Fiat} value={value} fractionDigits={USD_DECIMALS} />; + } + default: { + return <$Output type={OutputType.Text} value={value} />; + } + } +}; + +export const UnlaunchedMarketStatsDetails = ({ showMidMarketPrice = true }: ElementProps) => { + const stringGetter = useStringGetter(); + const { isTablet } = useBreakpoints(); + + // TODO: Replace with un launched market data + const { marketCap, spotVolume24H } = { + marketCap: 0, + spotVolume24H: 0, + }; + + const valueMap = { + [MarketStats.MARKET_CAP]: marketCap, + [MarketStats.SPOT_VOLUME_24H]: spotVolume24H, + }; + + const labelMap = { + [MarketStats.MARKET_CAP]: stringGetter({ key: STRING_KEYS.MARKET_CAP }), + [MarketStats.SPOT_VOLUME_24H]: stringGetter({ key: STRING_KEYS.SPOT_VOLUME_24H }), + }; + + return ( + <$MarketDetailsItems> + {showMidMarketPrice && ( + <$MidMarketPrice> + + + + )} + + <$Details + items={defaultMarketStatistics.map((stat) => ({ + key: stat, + label: labelMap[stat], + tooltip: stat, + value: , + }))} + layout={isTablet ? 'grid' : 'rowColumns'} + withSeparators={!isTablet} + /> + + ); +}; + +const $MarketDetailsItems = styled.div` + @media ${breakpoints.notTablet} { + ${layoutMixins.scrollArea} + ${layoutMixins.row} + isolation: isolate; + + align-items: stretch; + margin-left: 1px; + } + + @media ${breakpoints.tablet} { + border-bottom: solid var(--border-width) var(--color-border); + } +`; + +const $Details = styled(Details)` + font: var(--font-mini-book); + + @media ${breakpoints.tablet} { + ${layoutMixins.withOuterAndInnerBorders} + + font: var(--font-small-book); + + > * { + padding: 0.625rem 1rem; + } + } +`; + +const $MidMarketPrice = styled.div` + ${layoutMixins.sticky} + ${layoutMixins.row} + font: var(--font-medium-medium); + + background-color: var(--color-layer-2); + box-shadow: 0.25rem 0 0.75rem var(--color-layer-2); + padding-left: 1rem; + gap: 1rem; +`; + +const $Output = styled(Output)<{ color?: string }>` + ${layoutMixins.row} + + ${({ color }) => + color && + css` + color: ${color}; + `} +`; diff --git a/src/views/dialogs/LaunchMarketDialog.tsx b/src/views/dialogs/LaunchMarketDialog.tsx new file mode 100644 index 000000000..a4ab9ff2a --- /dev/null +++ b/src/views/dialogs/LaunchMarketDialog.tsx @@ -0,0 +1,21 @@ +import { DialogProps, LaunchMarketDialogProps } from '@/constants/dialogs'; + +import { useBreakpoints } from '@/hooks/useBreakpoints'; + +import { Dialog, DialogPlacement } from '@/components/Dialog'; + +import { NewMarketForm } from '../forms/NewMarketForm'; + +export const LaunchMarketDialog = ({ setIsOpen }: DialogProps) => { + const { isMobile } = useBreakpoints(); + + return ( + + + + ); +};