Merge pull request #148 from blockworks-foundation/trade-nav-concept
Trade page navigation concept
This commit is contained in:
commit
732c44dd39
|
@ -96,26 +96,28 @@ const ConnectWalletButton = () => {
|
|||
</Menu>
|
||||
) : (
|
||||
<div
|
||||
className="bg-th-bkg-1 h-14 flex divide-x divide-th-bkg-3 justify-between"
|
||||
className="h-14 flex divide-x divide-th-bkg-3 justify-between"
|
||||
id="connect-wallet-tip"
|
||||
>
|
||||
<button
|
||||
onClick={handleWalletConect}
|
||||
disabled={!wallet || !mangoGroup}
|
||||
className="rounded-none text-th-primary hover:bg-th-bkg-4 focus:outline-none disabled:text-th-fgd-4 disabled:cursor-wait"
|
||||
className="bg-th-primary rounded-none text-th-bkg-1 hover:brightness-[1.15] focus:outline-none disabled:text-th-fgd-4 disabled:cursor-wait"
|
||||
>
|
||||
<div className="flex flex-row items-center px-3 justify-center h-full default-transition hover:text-th-fgd-1">
|
||||
<div className="flex flex-row items-center px-3 justify-center h-full default-transition hover:text-th-bkg-1">
|
||||
<WalletIcon className="w-4 h-4 mr-2 fill-current" />
|
||||
<div className="text-left">
|
||||
<div className="mb-0.5 whitespace-nowrap">{t('connect')}</div>
|
||||
<div className="font-normal text-th-fgd-3 leading-3 tracking-wider text-xxs">
|
||||
<div className="font-bold mb-0.5 whitespace-nowrap">
|
||||
{t('connect')}
|
||||
</div>
|
||||
<div className="font-normal text-th-bkg-2 leading-3 tracking-wider text-xxs">
|
||||
{WALLET_PROVIDERS.find((p) => p.url === selectedWallet)?.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="relative">
|
||||
<WalletSelect isPrimary />
|
||||
<WalletSelect />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import { formatUsdValue } from '../utils'
|
||||
import { MarketDataLoader } from './MarketDetails'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
const DayHighLow = ({ high, low, latest }) => {
|
||||
const { t } = useTranslation('common')
|
||||
interface DayHighLowProps {
|
||||
high: number
|
||||
low: number
|
||||
latest: number
|
||||
isTableView?: boolean
|
||||
}
|
||||
|
||||
const DayHighLow = ({ high, low, latest, isTableView }: DayHighLowProps) => {
|
||||
let rangePercent = 0
|
||||
|
||||
if (high) {
|
||||
rangePercent =
|
||||
((parseFloat(latest) - parseFloat(low)) * 100) /
|
||||
(parseFloat(high) - parseFloat(low))
|
||||
rangePercent = ((latest - low) * 100) / (high - low)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between md:block md:pr-6">
|
||||
<div className="text-left xl:text-center text-th-fgd-3 tiny-text pb-0.5">
|
||||
{t('daily-range')}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="pr-2 text-th-fgd-2 md:text-xs">
|
||||
<div className={`pr-2 text-th-fgd-2 ${!isTableView && 'md:text-xs'}`}>
|
||||
{low ? formatUsdValue(low) : <MarketDataLoader />}
|
||||
</div>
|
||||
<div className="h-1.5 flex rounded bg-th-bkg-3 w-16 sm:w-16">
|
||||
|
@ -29,7 +29,7 @@ const DayHighLow = ({ high, low, latest }) => {
|
|||
className="flex rounded bg-th-primary"
|
||||
></div>
|
||||
</div>
|
||||
<div className="pl-2 text-th-fgd-2 md:text-xs">
|
||||
<div className={`pl-2 text-th-fgd-2 ${!isTableView && 'md:text-xs'}`}>
|
||||
{high ? formatUsdValue(high) : <MarketDataLoader />}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import { FAVORITE_MARKETS_KEY } from './TradeNavMenu'
|
||||
import { StarIcon } from '@heroicons/react/solid'
|
||||
import { QuestionMarkCircleIcon } from '@heroicons/react/outline'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { useViewport } from '../hooks/useViewport'
|
||||
import { breakpoints } from './TradePageGrid'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { initialMarket } from './SettingsModal'
|
||||
import * as MonoIcons from './icons'
|
||||
import { Transition } from '@headlessui/react'
|
||||
|
||||
const FavoritesShortcutBar = () => {
|
||||
const [favoriteMarkets] = useLocalStorageState(FAVORITE_MARKETS_KEY, [])
|
||||
const marketInfo = useMangoStore((s) => s.marketInfo)
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
const { asPath } = useRouter()
|
||||
|
||||
const renderIcon = (symbol) => {
|
||||
const iconName = `${symbol.slice(0, 1)}${symbol
|
||||
.slice(1, 4)
|
||||
.toLowerCase()}MonoIcon`
|
||||
|
||||
const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon
|
||||
return <SymbolIcon className={`h-3.5 w-auto mr-2`} />
|
||||
}
|
||||
|
||||
return !isMobile ? (
|
||||
<Transition
|
||||
appear={true}
|
||||
className="bg-th-bkg-3 flex items-center px-4 xl:px-6 py-2 space-x-4"
|
||||
show={favoriteMarkets.length > 0}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<StarIcon className="h-5 text-th-fgd-4 w-5" />
|
||||
{favoriteMarkets.map((mkt) => {
|
||||
const mktInfo = marketInfo.find((info) => info.name === mkt.name)
|
||||
return (
|
||||
<Link href={`/?name=${mkt.name}`} key={mkt.name} shallow={true}>
|
||||
<a
|
||||
className={`flex items-center py-1 text-xs hover:text-th-primary whitespace-nowrap ${
|
||||
asPath.includes(mkt.name) ||
|
||||
(asPath === '/' && initialMarket.name === mkt.name)
|
||||
? 'text-th-primary'
|
||||
: 'text-th-fgd-3'
|
||||
}`}
|
||||
>
|
||||
{renderIcon(mkt.baseSymbol)}
|
||||
<span className="mb-0 mr-1.5 text-xs">{mkt.name}</span>
|
||||
<span
|
||||
className={`text-xs ${
|
||||
mktInfo
|
||||
? mktInfo.change24h >= 0
|
||||
? 'text-th-green'
|
||||
: 'text-th-red'
|
||||
: 'text-th-fgd-4'
|
||||
}`}
|
||||
>
|
||||
{mktInfo ? `${(mktInfo.change24h * 100).toFixed(1)}%` : ''}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</Transition>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default FavoritesShortcutBar
|
|
@ -1,3 +1,5 @@
|
|||
import { forwardRef } from 'react'
|
||||
|
||||
interface InputProps {
|
||||
type: string
|
||||
value: any
|
||||
|
@ -9,23 +11,19 @@ interface InputProps {
|
|||
[x: string]: any
|
||||
}
|
||||
|
||||
const Group = ({ children, className = '' }) => {
|
||||
return <div className={`flex ${className}`}>{children}</div>
|
||||
}
|
||||
|
||||
const Input = ({
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
error,
|
||||
wrapperClassName = 'w-full',
|
||||
disabled,
|
||||
prefix,
|
||||
prefixClassName,
|
||||
suffix,
|
||||
...props
|
||||
}: InputProps) => {
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const {
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
error,
|
||||
wrapperClassName = 'w-full',
|
||||
disabled,
|
||||
prefix,
|
||||
prefixClassName,
|
||||
suffix,
|
||||
} = props
|
||||
return (
|
||||
<div className={`flex relative ${wrapperClassName}`}>
|
||||
{prefix ? (
|
||||
|
@ -51,6 +49,7 @@ const Input = ({
|
|||
}
|
||||
${prefix ? 'pl-7' : ''}`}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{suffix ? (
|
||||
|
@ -60,8 +59,6 @@ const Input = ({
|
|||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Input.Group = Group
|
||||
})
|
||||
|
||||
export default Input
|
||||
|
|
|
@ -18,11 +18,11 @@ import BN from 'bn.js'
|
|||
import { useViewport } from '../hooks/useViewport'
|
||||
import { breakpoints } from './TradePageGrid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import SwitchMarketDropdown from './SwitchMarketDropdown'
|
||||
import Tooltip from './Tooltip'
|
||||
import { SECONDS } from '../stores/useMangoStore'
|
||||
|
||||
const SECONDS = 1000
|
||||
|
||||
function calculateFundingRate(perpStats, perpMarket) {
|
||||
export function calculateFundingRate(perpStats, perpMarket) {
|
||||
const oldestStat = perpStats[perpStats.length - 1]
|
||||
const latestStat = perpStats[0]
|
||||
|
||||
|
@ -48,7 +48,7 @@ function calculateFundingRate(perpStats, perpMarket) {
|
|||
return (fundingInQuoteDecimals / basePriceInBaseLots) * 100
|
||||
}
|
||||
|
||||
function parseOpenInterest(perpMarket: PerpMarket) {
|
||||
export function parseOpenInterest(perpMarket: PerpMarket) {
|
||||
if (!perpMarket || !(perpMarket instanceof PerpMarket)) return 0
|
||||
|
||||
return perpMarket.baseLotsToNumber(perpMarket.openInterest) / 2
|
||||
|
@ -180,21 +180,24 @@ const MarketDetails = () => {
|
|||
<div className="flex flex-col lg:flex-row lg:items-center">
|
||||
<div className="hidden md:block md:pb-4 md:pr-6 lg:pb-0">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="24"
|
||||
height="24"
|
||||
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="24"
|
||||
height="24"
|
||||
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
|
||||
<div className="font-semibold pr-0.5 text-xl">{baseSymbol}</div>
|
||||
<span className="text-th-fgd-4 text-xl">
|
||||
{isPerpMarket ? '-' : '/'}
|
||||
</span>
|
||||
<div className="font-semibold pl-0.5 text-xl">
|
||||
{isPerpMarket ? 'PERP' : groupConfig.quoteSymbol}
|
||||
<div className="font-semibold pr-0.5 text-xl">{baseSymbol}</div>
|
||||
<span className="text-th-fgd-4 text-xl">
|
||||
{isPerpMarket ? '-' : '/'}
|
||||
</span>
|
||||
<div className="font-semibold pl-0.5 text-xl">
|
||||
{isPerpMarket ? 'PERP' : groupConfig.quoteSymbol}
|
||||
</div>
|
||||
</div>
|
||||
<SwitchMarketDropdown />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-flow-row grid-cols-1 md:grid-cols-3 gap-3 lg:grid-cols-none lg:grid-flow-col lg:grid-rows-1 lg:gap-6">
|
||||
|
@ -277,20 +280,25 @@ const MarketDetails = () => {
|
|||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<DayHighLow
|
||||
high={ohlcv?.h[0]}
|
||||
low={ohlcv?.l[0]}
|
||||
latest={oraclePrice?.toNumber()}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-left xl:text-center text-th-fgd-3 tiny-text pb-0.5">
|
||||
{t('daily-range')}
|
||||
</div>
|
||||
<DayHighLow
|
||||
high={ohlcv?.h[0]}
|
||||
low={ohlcv?.l[0]}
|
||||
latest={oraclePrice?.toNumber()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute right-4 bottom-0 sm:bottom-auto lg:right-6 flex items-center justify-end">
|
||||
<div className="absolute right-0 bottom-0 sm:bottom-auto lg:right-3 flex items-center justify-end space-x-2">
|
||||
{!isMobile ? (
|
||||
<div id="layout-tip">
|
||||
<UiLock />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="ml-2" id="data-refresh-tip">
|
||||
<div id="data-refresh-tip">
|
||||
{!isMobile && connected ? <ManualRefresh /> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import { FunctionComponent, RefObject } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { initialMarket } from './SettingsModal'
|
||||
import { FavoriteMarketButton } from './TradeNavMenu'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { getWeights } from '@blockworks-foundation/mango-client'
|
||||
|
||||
interface MarketNavItemProps {
|
||||
market: any
|
||||
onClick?: () => void
|
||||
buttonRef?: RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
const MarketNavItem: FunctionComponent<MarketNavItemProps> = ({
|
||||
market,
|
||||
onClick,
|
||||
buttonRef,
|
||||
}) => {
|
||||
const { asPath } = useRouter()
|
||||
const router = useRouter()
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const marketInfo = useMangoStore((s) => s.marketInfo)
|
||||
|
||||
const mktInfo = marketInfo.find((info) => info.name === market.name)
|
||||
|
||||
const selectMarket = (market) => {
|
||||
buttonRef?.current?.click()
|
||||
router.push(`/?name=${market.name}`, undefined, { shallow: true })
|
||||
if (onClick) {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
|
||||
const getMarketLeverage = (market) => {
|
||||
if (!mangoGroup) return 1
|
||||
const ws = getWeights(mangoGroup, market.marketIndex, 'Init')
|
||||
const w = market.name.includes('PERP')
|
||||
? ws.perpAssetWeight
|
||||
: ws.spotAssetWeight
|
||||
return Math.round((100 * -1) / (w.toNumber() - 1)) / 100
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-th-fgd-3">
|
||||
<div className="flex items-center">
|
||||
<FavoriteMarketButton market={market} />
|
||||
<button
|
||||
className="font-normal flex items-center justify-between w-full"
|
||||
onClick={() => selectMarket(market)}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center text-xs hover:text-th-primary w-full whitespace-nowrap ${
|
||||
asPath.includes(market.name) ||
|
||||
(asPath === '/' && initialMarket.name === market.name)
|
||||
? 'text-th-primary'
|
||||
: 'text-th-fgd-1'
|
||||
}`}
|
||||
>
|
||||
<span className="ml-2">{market.name}</span>
|
||||
<span className="ml-1.5 text-xs text-th-fgd-4">
|
||||
{getMarketLeverage(market)}x
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`text-xs ${
|
||||
mktInfo
|
||||
? mktInfo.change24h >= 0
|
||||
? 'text-th-green'
|
||||
: 'text-th-red'
|
||||
: 'text-th-fgd-4'
|
||||
}`}
|
||||
>
|
||||
{mktInfo ? `${(mktInfo.change24h * 100).toFixed(1)}%` : '?'}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketNavItem
|
|
@ -8,7 +8,7 @@ const MenuItem = ({ href, children, newWindow = false }) => {
|
|||
return (
|
||||
<Link href={href} shallow={true}>
|
||||
<a
|
||||
className={`h-full border-b border-th-bkg-4 md:border-none flex justify-between text-th-fgd-1 font-bold items-center md:px-2 lg:px-4 py-3 md:py-0 hover:text-th-primary
|
||||
className={`h-full border-b border-th-bkg-4 md:border-none flex justify-between text-th-fgd-1 font-bold items-center p-3 md:py-0 hover:text-th-primary
|
||||
${asPath === href ? `text-th-primary` : `border-transparent`}
|
||||
`}
|
||||
target={newWindow ? '_blank' : ''}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useRef, useState } from 'react'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline'
|
||||
import { Fragment, useRef } from 'react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid'
|
||||
import Link from 'next/link'
|
||||
|
||||
type NavDropMenuProps = {
|
||||
|
@ -13,52 +13,59 @@ export default function NavDropMenu({
|
|||
linksArray = [],
|
||||
}: NavDropMenuProps) {
|
||||
const buttonRef = useRef(null)
|
||||
const [openState, setOpenState] = useState(false)
|
||||
|
||||
const toggleMenu = () => {
|
||||
setOpenState((openState) => !openState)
|
||||
buttonRef?.current?.click()
|
||||
}
|
||||
|
||||
const onHover = (open, action) => {
|
||||
if (
|
||||
(!open && !openState && action === 'onMouseEnter') ||
|
||||
(open && openState && action === 'onMouseLeave')
|
||||
(!open && action === 'onMouseEnter') ||
|
||||
(open && action === 'onMouseLeave')
|
||||
) {
|
||||
toggleMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (open) => {
|
||||
setOpenState(!open)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<div
|
||||
onMouseEnter={() => onHover(open, 'onMouseEnter')}
|
||||
onMouseLeave={() => onHover(open, 'onMouseLeave')}
|
||||
className="flex flex-col"
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<div
|
||||
onMouseEnter={() => onHover(open, 'onMouseEnter')}
|
||||
onMouseLeave={() => onHover(open, 'onMouseLeave')}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<Popover.Button
|
||||
className={`-mr-3 px-3 rounded-none focus:outline-none focus:bg-th-bkg-3 ${
|
||||
open && 'bg-th-bkg-3'
|
||||
}`}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<Popover.Button
|
||||
className="h-10 text-th-fgd-1 hover:text-th-primary md:px-2 lg:px-4 focus:outline-none transition-none"
|
||||
ref={buttonRef}
|
||||
<div
|
||||
className={`flex font-bold h-14 items-center rounded-none hover:text-th-primary`}
|
||||
>
|
||||
<div
|
||||
className="flex items-center"
|
||||
onClick={() => handleClick(open)}
|
||||
>
|
||||
<span className="font-bold">{menuTitle}</span>
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 default-transition ml-1.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute top-10 z-10">
|
||||
<div className="relative bg-th-bkg-2 divide-y divide-th-bkg-3 px-4 rounded">
|
||||
<span className="font-bold">{menuTitle}</span>
|
||||
<ChevronDownIcon
|
||||
className={`default-transition h-5 ml-0.5 w-5 ${
|
||||
open ? 'transform rotate-180' : 'transform rotate-360'
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0 transform scale-75"
|
||||
enterTo="opacity-100 transform scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Panel className="absolute top-14 z-10">
|
||||
<div className="relative bg-th-bkg-3 divide-y divide-th-bkg-3 px-4 rounded-b-md">
|
||||
{linksArray.map(([name, href, isExternal]) =>
|
||||
!isExternal ? (
|
||||
<Link href={href} key={href}>
|
||||
|
@ -80,9 +87,9 @@ export default function NavDropMenu({
|
|||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
import { Fragment, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { SearchIcon } from '@heroicons/react/outline'
|
||||
import { SwitchHorizontalIcon, XIcon } from '@heroicons/react/solid'
|
||||
import Input from './Input'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import MarketNavItem from './MarketNavItem'
|
||||
|
||||
const SwitchMarketDropdown = () => {
|
||||
const groupConfig = useMangoGroupConfig()
|
||||
const markets = useMemo(
|
||||
() => [...groupConfig.spotMarkets, ...groupConfig.perpMarkets],
|
||||
[groupConfig]
|
||||
)
|
||||
const spotMarkets = useMemo(
|
||||
() =>
|
||||
[...groupConfig.spotMarkets].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[groupConfig]
|
||||
)
|
||||
const perpMarkets = useMemo(
|
||||
() =>
|
||||
[...groupConfig.perpMarkets].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[groupConfig]
|
||||
)
|
||||
|
||||
const [suggestions, setSuggestions] = useState([])
|
||||
const [searchString, setSearchString] = useState('')
|
||||
const buttonRef = useRef(null)
|
||||
const { t } = useTranslation('common')
|
||||
const filteredMarkets = markets
|
||||
.filter((m) => m.name.toLowerCase().includes(searchString.toLowerCase()))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
const onSearch = (searchString) => {
|
||||
if (searchString.length > 0) {
|
||||
const newSuggestions = suggestions.filter((v) =>
|
||||
v.name.toLowerCase().includes(searchString.toLowerCase())
|
||||
)
|
||||
setSuggestions(newSuggestions)
|
||||
}
|
||||
setSearchString(searchString)
|
||||
}
|
||||
|
||||
const callbackRef = useCallback((inputElement) => {
|
||||
if (inputElement) {
|
||||
const timer = setTimeout(() => inputElement.focus(), 200)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
{({ open }) => (
|
||||
<div className="flex flex-col ml-2 relative">
|
||||
<Popover.Button
|
||||
className={`focus:outline-none focus:bg-th-bkg-3 ${
|
||||
open && 'bg-th-bkg-3'
|
||||
}`}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div
|
||||
className={`flex h-10 items-center justify-center rounded-none w-10 hover:text-th-primary`}
|
||||
>
|
||||
{open ? (
|
||||
<XIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<SwitchHorizontalIcon className="h-5 w-5" />
|
||||
)}
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0 transform scale-75"
|
||||
enterTo="opacity-100 transform scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Panel
|
||||
className="absolute bg-th-bkg-3 max-h-96 overflow-y-auto p-4 left-1/2 transform -translate-x-1/2 rounded-b-md rounded-tl-md thin-scroll top-12 w-72 z-10"
|
||||
static
|
||||
>
|
||||
<div className="pb-2.5">
|
||||
<Input
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
prefix={<SearchIcon className="h-4 text-th-fgd-3 w-4" />}
|
||||
ref={callbackRef}
|
||||
type="text"
|
||||
value={searchString}
|
||||
/>
|
||||
</div>
|
||||
{searchString.length > 0 ? (
|
||||
<div className="pt-1.5 space-y-2.5">
|
||||
{filteredMarkets.length > 0 ? (
|
||||
filteredMarkets.map((mkt) => (
|
||||
<MarketNavItem
|
||||
buttonRef={buttonRef}
|
||||
onClick={() => setSearchString('')}
|
||||
market={mkt}
|
||||
key={mkt.name}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="mb-0 text-center">{t('no-markets')}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex justify-between pt-1.5">
|
||||
<h4 className="text-xs">{t('spot')}</h4>
|
||||
<p className="mb-0 text-th-fgd-4 text-xs">
|
||||
{t('rolling-change')}
|
||||
</p>
|
||||
</div>
|
||||
{spotMarkets.map((mkt) => (
|
||||
<MarketNavItem
|
||||
buttonRef={buttonRef}
|
||||
onClick={() => setSearchString('')}
|
||||
market={mkt}
|
||||
key={mkt.name}
|
||||
/>
|
||||
))}
|
||||
<div className="flex justify-between pt-1.5">
|
||||
<h4 className="text-xs">{t('perp')}</h4>
|
||||
<p className="mb-0 text-th-fgd-4 text-xs">
|
||||
{t('rolling-change')}
|
||||
</p>
|
||||
</div>
|
||||
{perpMarkets.map((mkt) => (
|
||||
<MarketNavItem
|
||||
buttonRef={buttonRef}
|
||||
onClick={() => setSearchString('')}
|
||||
market={mkt}
|
||||
key={mkt.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwitchMarketDropdown
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { abbreviateAddress } from '../utils/index'
|
||||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
|
@ -12,6 +12,8 @@ import LanguageSwitch from './LanguageSwitch'
|
|||
import { DEFAULT_MARKET_KEY, initialMarket } from './SettingsModal'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Settings from './Settings'
|
||||
import TradeNavMenu from './TradeNavMenu'
|
||||
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
|
||||
|
||||
const StyledNewLabel = ({ children, ...props }) => (
|
||||
<div style={{ fontSize: '0.5rem', marginLeft: '1px' }} {...props}>
|
||||
|
@ -28,15 +30,22 @@ const TopBar = () => {
|
|||
DEFAULT_MARKET_KEY,
|
||||
initialMarket
|
||||
)
|
||||
const actions = useMangoStore((s) => s.actions)
|
||||
const groupConfig = useMangoGroupConfig()
|
||||
const markets = [...groupConfig.spotMarkets, ...groupConfig.perpMarkets]
|
||||
|
||||
const handleCloseAccounts = useCallback(() => {
|
||||
setShowAccountsModal(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
actions.fetchMarketInfo(markets)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className={`bg-th-bkg-2 border-b border-th-bkg-2`}>
|
||||
<div className={`px-4 lg:px-10`}>
|
||||
<nav className={`bg-th-bkg-2`}>
|
||||
<div className={`px-4 xl:px-6`}>
|
||||
<div className={`flex justify-between h-14`}>
|
||||
<div className={`flex`}>
|
||||
<Link href={defaultMarket.path} shallow={true}>
|
||||
|
@ -50,8 +59,10 @@ const TopBar = () => {
|
|||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className={`hidden md:flex md:items-center md:ml-4`}>
|
||||
<MenuItem href={defaultMarket.path}>{t('trade')}</MenuItem>
|
||||
<div
|
||||
className={`hidden md:flex md:items-center md:space-x-2 lg:space-x-3 md:ml-4`}
|
||||
>
|
||||
<TradeNavMenu />
|
||||
<MenuItem href="/swap">{t('swap')}</MenuItem>
|
||||
<MenuItem href="/account">{t('account')}</MenuItem>
|
||||
<MenuItem href="/borrow">{t('borrow')}</MenuItem>
|
||||
|
@ -59,12 +70,10 @@ const TopBar = () => {
|
|||
<div className="relative">
|
||||
<MenuItem href="/referral">
|
||||
{t('referrals')}
|
||||
<div>
|
||||
<div className="absolute flex items-center justify-center h-4 px-1.5 bg-gradient-to-br from-red-500 to-yellow-500 rounded-full -right-2 -top-3">
|
||||
<StyledNewLabel className="text-white uppercase">
|
||||
new
|
||||
</StyledNewLabel>
|
||||
</div>
|
||||
<div className="absolute flex items-center justify-center h-4 px-1.5 bg-gradient-to-br from-red-500 to-yellow-500 rounded-full -right-3 -top-3">
|
||||
<StyledNewLabel className="text-white uppercase">
|
||||
new
|
||||
</StyledNewLabel>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
|
@ -108,7 +117,7 @@ const TopBar = () => {
|
|||
</div>
|
||||
) : null}
|
||||
<div className="flex">
|
||||
<div className="pl-2">
|
||||
<div className="pl-4">
|
||||
<ConnectWalletButton />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
import { Fragment, FunctionComponent, useEffect, useRef, useState } from 'react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { StarIcon } from '@heroicons/react/outline'
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
StarIcon as FilledStarIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
|
||||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import MarketNavItem from './MarketNavItem'
|
||||
|
||||
const initialMenuCategories = [
|
||||
{ name: 'Perp', desc: 'perp-desc' },
|
||||
{ name: 'Spot', desc: 'spot-desc' },
|
||||
]
|
||||
|
||||
export const FAVORITE_MARKETS_KEY = 'favoriteMarkets'
|
||||
|
||||
const TradeNavMenu = () => {
|
||||
const [favoriteMarkets] = useLocalStorageState(FAVORITE_MARKETS_KEY, [])
|
||||
const [activeMenuCategory, setActiveMenuCategory] = useState('Perp')
|
||||
const [menuCategories, setMenuCategories] = useState(initialMenuCategories)
|
||||
const buttonRef = useRef(null)
|
||||
const groupConfig = useMangoGroupConfig()
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const markets =
|
||||
activeMenuCategory === 'Favorites'
|
||||
? favoriteMarkets
|
||||
: activeMenuCategory === 'Spot'
|
||||
? [...groupConfig.spotMarkets]
|
||||
: [...groupConfig.perpMarkets]
|
||||
|
||||
const handleMenuCategoryChange = (categoryName) => {
|
||||
setActiveMenuCategory(categoryName)
|
||||
}
|
||||
|
||||
const toggleMenu = () => {
|
||||
buttonRef?.current?.click()
|
||||
if (favoriteMarkets.length > 0) {
|
||||
setActiveMenuCategory('Favorites')
|
||||
} else {
|
||||
setActiveMenuCategory('Perp')
|
||||
}
|
||||
}
|
||||
|
||||
const onHoverMenu = (open, action) => {
|
||||
if (
|
||||
(!open && action === 'onMouseEnter') ||
|
||||
(open && action === 'onMouseLeave')
|
||||
) {
|
||||
toggleMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (buttonRef.current && !buttonRef.current.contains(event.target)) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (favoriteMarkets.length > 0 && menuCategories.length === 2) {
|
||||
const newCategories = [{ name: 'Favorites', desc: '' }, ...menuCategories]
|
||||
setMenuCategories(newCategories)
|
||||
}
|
||||
if (favoriteMarkets.length === 0 && menuCategories.length === 3) {
|
||||
setMenuCategories(
|
||||
menuCategories.filter((cat) => cat.name !== 'Favorites')
|
||||
)
|
||||
if (activeMenuCategory === 'Favorites') {
|
||||
setActiveMenuCategory('Perp')
|
||||
}
|
||||
}
|
||||
}, [favoriteMarkets])
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
{({ open }) => (
|
||||
<div
|
||||
onMouseEnter={() => onHoverMenu(open, 'onMouseEnter')}
|
||||
onMouseLeave={() => onHoverMenu(open, 'onMouseLeave')}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<Popover.Button
|
||||
className={`-mr-3 px-3 rounded-none focus:outline-none focus:bg-th-bkg-3 ${
|
||||
open && 'bg-th-bkg-3'
|
||||
}`}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div
|
||||
className={`flex font-bold h-14 items-center rounded-none hover:text-th-primary`}
|
||||
>
|
||||
<span>{t('trade')}</span>
|
||||
<ChevronDownIcon
|
||||
className={`default-transition h-5 ml-0.5 w-5 ${
|
||||
open ? 'transform rotate-180' : 'transform rotate-360'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0 transform scale-75"
|
||||
enterTo="opacity-100 transform scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Panel className="absolute grid grid-cols-3 grid-rows-1 min-h-[235px] top-14 w-[700px] z-10">
|
||||
<div className="bg-th-bkg-4 col-span-1 rounded-bl-lg">
|
||||
<MenuCategories
|
||||
activeCategory={activeMenuCategory}
|
||||
categories={menuCategories}
|
||||
onChange={handleMenuCategoryChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-th-bkg-3 col-span-2 p-4 rounded-br-lg">
|
||||
<div className="grid grid-cols-2 grid-flow-row gap-x-6 gap-y-2.5">
|
||||
{markets.map((mkt) => (
|
||||
<MarketNavItem
|
||||
buttonRef={buttonRef}
|
||||
market={mkt}
|
||||
key={mkt.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default TradeNavMenu
|
||||
|
||||
interface MenuCategoriesProps {
|
||||
activeCategory: string
|
||||
onChange: (x) => void
|
||||
categories: Array<any>
|
||||
}
|
||||
|
||||
const MenuCategories: FunctionComponent<MenuCategoriesProps> = ({
|
||||
activeCategory,
|
||||
onChange,
|
||||
categories,
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<div className={`relative`}>
|
||||
<div
|
||||
className={`absolute bg-th-primary top-0 default-transition left-0 w-0.5 z-10`}
|
||||
style={{
|
||||
transform: `translateY(${
|
||||
categories.findIndex((cat) => cat.name === activeCategory) * 100
|
||||
}%)`,
|
||||
height: `${100 / categories.length}%`,
|
||||
}}
|
||||
/>
|
||||
{categories.map((cat) => {
|
||||
return (
|
||||
<button
|
||||
key={cat.name}
|
||||
onClick={() => onChange(cat.name)}
|
||||
onMouseEnter={() => onChange(cat.name)}
|
||||
className={`cursor-pointer default-transition flex flex-col h-14 justify-center px-4 relative rounded-none w-full whitespace-nowrap hover:bg-th-bkg-3 ${
|
||||
activeCategory === cat.name
|
||||
? `bg-th-bkg-3 text-th-primary`
|
||||
: `text-th-fgd-2 hover:text-th-primary`
|
||||
}
|
||||
`}
|
||||
>
|
||||
{t(cat.name.toLowerCase().replace(' ', '-'))}
|
||||
<div className="font-normal text-th-fgd-4 text-xs">
|
||||
{t(cat.desc)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FavoriteMarketButton = ({ market }) => {
|
||||
const [favoriteMarkets, setFavoriteMarkets] = useLocalStorageState(
|
||||
FAVORITE_MARKETS_KEY,
|
||||
[]
|
||||
)
|
||||
|
||||
const addToFavorites = (mkt) => {
|
||||
const newFavorites = [...favoriteMarkets, mkt]
|
||||
setFavoriteMarkets(newFavorites)
|
||||
}
|
||||
|
||||
const removeFromFavorites = (mkt) => {
|
||||
setFavoriteMarkets(favoriteMarkets.filter((m) => m.name !== mkt.name))
|
||||
}
|
||||
|
||||
return favoriteMarkets.find((mkt) => mkt.name === market.name) ? (
|
||||
<button
|
||||
className="default-transition text-th-primary hover:text-th-fgd-3"
|
||||
onClick={() => removeFromFavorites(market)}
|
||||
>
|
||||
<FilledStarIcon className="h-5 w-5" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="default-transition text-th-fgd-4 hover:text-th-primary"
|
||||
onClick={() => addToFavorites(market)}
|
||||
>
|
||||
<StarIcon className="h-5 w-5" />
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -110,11 +110,14 @@ const TradePageGrid = () => {
|
|||
}, [currentBreakpoint, savedLayouts])
|
||||
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
return !isMobile ? (
|
||||
<>
|
||||
<MarketDetails />
|
||||
<div className="pt-2">
|
||||
<MarketDetails />
|
||||
</div>
|
||||
<ResponsiveGridLayout
|
||||
layouts={savedLayouts ? savedLayouts : defaultLayouts}
|
||||
breakpoints={breakpoints}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Menu } from '@headlessui/react'
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
||||
import { Fragment } from 'react'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { WALLET_PROVIDERS } from '../utils/wallet-adapters'
|
||||
|
||||
export default function WalletSelect({ isPrimary = false }) {
|
||||
export default function WalletSelect() {
|
||||
const setMangoStore = useMangoStore((s) => s.set)
|
||||
|
||||
const handleSelectProvider = (url) => {
|
||||
|
@ -17,33 +18,41 @@ export default function WalletSelect({ isPrimary = false }) {
|
|||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={`flex justify-center items-center h-full rounded-none focus:outline-none text-th-primary hover:text-th-fgd-1 ${
|
||||
isPrimary
|
||||
? 'px-3 hover:bg-th-bkg-4'
|
||||
: 'px-2 hover:bg-th-bkg-4 border-l border-th-fgd-4'
|
||||
} cursor-pointer`}
|
||||
className={`bg-th-primary flex justify-center items-center h-full rounded-none focus:outline-none text-th-bkg-1 hover:brightness-[1.15] hover:text-th-bkg-1 hover:bg-th-primary cursor-pointer w-10`}
|
||||
>
|
||||
{open ? (
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
className={`default-transition h-5 w-5 ${
|
||||
open ? 'transform rotate-180' : 'transform rotate-360'
|
||||
}`}
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute bg-th-bkg-1 divide-y divide-th-bkg-3 p-1 rounded-md right-0.5 mt-1 shadow-lg outline-none w-36 z-20">
|
||||
{WALLET_PROVIDERS.map(({ name, url, icon }) => (
|
||||
<Menu.Item key={name}>
|
||||
<button
|
||||
className="flex flex-row items-center justify-between rounded-none text-xs w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer font-normal focus:outline-none"
|
||||
onClick={() => handleSelectProvider(url)}
|
||||
>
|
||||
<div className="flex">
|
||||
<img src={icon} className="w-4 h-4 mr-2" />
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0 transform scale-75"
|
||||
enterTo="opacity-100 transform scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Menu.Items className="absolute bg-th-bkg-1 divide-y divide-th-bkg-3 rounded-md right-0 mt-1 shadow-lg outline-none w-44 z-20">
|
||||
{WALLET_PROVIDERS.map(({ name, url, icon }) => (
|
||||
<Menu.Item key={name}>
|
||||
<button
|
||||
className="flex flex-row items-center justify-between rounded-none text-xs w-full p-2 hover:bg-th-bkg-2 hover:cursor-pointer font-normal focus:outline-none"
|
||||
onClick={() => handleSelectProvider(url)}
|
||||
>
|
||||
<div className="flex">
|
||||
<img src={icon} className="w-4 h-4 mr-2" />
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import { Disclosure } from '@headlessui/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { XIcon } from '@heroicons/react/outline'
|
||||
import { SwitchHorizontalIcon, XIcon } from '@heroicons/react/outline'
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import { getWeights, PerpMarket } from '@blockworks-foundation/mango-client'
|
||||
import { CandlesIcon } from '../icons'
|
||||
|
@ -55,34 +55,37 @@ const MobileTradePage = () => {
|
|||
return (
|
||||
<div className="pb-14 pt-4 px-2">
|
||||
<div className="relative">
|
||||
<Link href="/select">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="30"
|
||||
height="30"
|
||||
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
|
||||
className="mr-2"
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="30"
|
||||
height="30"
|
||||
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
|
||||
className="mr-2"
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<div className="font-semibold pr-0.5 text-xl">{baseSymbol}</div>
|
||||
<span className="text-th-fgd-4 text-xl">
|
||||
{isPerpMarket ? '-' : '/'}
|
||||
</span>
|
||||
<div className="font-semibold pl-0.5 text-xl">
|
||||
{isPerpMarket ? 'PERP' : groupConfig.quoteSymbol}
|
||||
</div>
|
||||
</div>
|
||||
<span className="border border-th-primary ml-2 px-1 py-0.5 rounded text-xs text-th-primary">
|
||||
{initLeverage}x
|
||||
<div className="font-semibold pr-0.5 text-xl">{baseSymbol}</div>
|
||||
<span className="text-th-fgd-4 text-xl">
|
||||
{isPerpMarket ? '-' : '/'}
|
||||
</span>
|
||||
<div className="font-semibold pl-0.5 text-xl">
|
||||
{isPerpMarket ? 'PERP' : groupConfig.quoteSymbol}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<span className="border border-th-primary ml-2 px-1 py-0.5 rounded text-xs text-th-primary">
|
||||
{initLeverage}x
|
||||
</span>
|
||||
<Link href="/select">
|
||||
<div className="flex items-center justify-center h-10 ml-2 w-10">
|
||||
<SwitchHorizontalIcon className="h-5 w-5" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button>
|
||||
<div className="absolute right-0 top-0 bg-th-bkg-4 flex items-center justify-center rounded-full w-8 h-8 text-th-fgd-1 focus:outline-none hover:text-th-primary">
|
||||
<div className="absolute right-0 top-1 bg-th-bkg-4 flex items-center justify-center rounded-full w-8 h-8 text-th-fgd-1 focus:outline-none hover:text-th-primary">
|
||||
{open ? (
|
||||
<XIcon className="h-4 w-4" />
|
||||
) : (
|
||||
|
|
|
@ -19,8 +19,7 @@ import {
|
|||
marketSelector,
|
||||
marketsSelector,
|
||||
} from '../stores/selectors'
|
||||
|
||||
const SECONDS = 1000
|
||||
import { SECONDS } from '../stores/useMangoStore'
|
||||
|
||||
function decodeBook(market, accInfo: AccountInfo<Buffer>): number[][] {
|
||||
if (market && accInfo?.data) {
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
} from '@blockworks-foundation/mango-client'
|
||||
import TopBar from '../components/TopBar'
|
||||
import TradePageGrid from '../components/TradePageGrid'
|
||||
import MarketSelect from '../components/MarketSelect'
|
||||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import AlphaModal, { ALPHA_MODAL_KEY } from '../components/AlphaModal'
|
||||
import { PageBodyWrapper } from '../components/styles'
|
||||
|
@ -23,6 +22,7 @@ import {
|
|||
walletConnectedSelector,
|
||||
} from '../stores/selectors'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import FavoritesShortcutBar from '../components/FavoritesShortcutBar'
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
|
@ -132,8 +132,8 @@ const PerpMarket = () => {
|
|||
<IntroTips connected={connected} mangoAccount={mangoAccount} />
|
||||
) : null}
|
||||
<TopBar />
|
||||
<MarketSelect />
|
||||
<PageBodyWrapper className="p-1 sm:px-2 sm:py-1 md:px-2 md:py-1">
|
||||
<FavoritesShortcutBar />
|
||||
<PageBodyWrapper className="p-1 sm:px-2 sm:py-1 md:px-2 md:py-1 xl:px-4">
|
||||
<TradePageGrid />
|
||||
</PageBodyWrapper>
|
||||
{!alphaAccepted && (
|
||||
|
|
|
@ -94,6 +94,8 @@
|
|||
"current-stats": "Current Stats",
|
||||
"custom": "Custom",
|
||||
"daily-change": "Daily Change",
|
||||
"daily-high": "24hr High",
|
||||
"daily-low": "24hr Low",
|
||||
"daily-range": "Daily Range",
|
||||
"daily-volume": "24hr Volume",
|
||||
"dark": "Dark",
|
||||
|
@ -135,6 +137,8 @@
|
|||
"export-data": "Export CSV",
|
||||
"export-data-empty": "No data to export",
|
||||
"export-data-success": "CSV exported successfully",
|
||||
"favorite": "Favorite",
|
||||
"favorites": "Favorites",
|
||||
"fee": "Fee",
|
||||
"fee-discount": "Fee Discount",
|
||||
"first-deposit-desc": "There is a one-time cost of 0.035 SOL when you make your first deposit. This covers the rent on the Solana Blockchain for your account.",
|
||||
|
@ -243,6 +247,7 @@
|
|||
"no-history": "No trade history",
|
||||
"no-interest": "No interest earned or paid",
|
||||
"no-margin": "No margin accounts found",
|
||||
"no-markets": "No markets found",
|
||||
"no-orders": "No open orders",
|
||||
"no-perp": "No perp positions",
|
||||
"no-unsettled": "There are no unsettled funds",
|
||||
|
@ -263,6 +268,7 @@
|
|||
"performance-insights": "Performance Insights",
|
||||
"period-progress": "Period Progress",
|
||||
"perp": "Perp",
|
||||
"perp-desc": "Perpetual swaps settled in USDC",
|
||||
"perp-fees": "Mango Perp Fees",
|
||||
"perp-positions": "Perp Positions",
|
||||
"perp-positions-tip-desc": "Perp positions accrue Unsettled PnL as price moves. Settling PnL adds or removes that amount from your USDC balance.",
|
||||
|
@ -330,6 +336,7 @@
|
|||
"slippage-warning": "This order will likely have extremely large slippage! Consider using Stop Limit or Take Profit Limit order instead.",
|
||||
"spanish": "Español",
|
||||
"spot": "Spot",
|
||||
"spot-desc": "Spot margin quoted in USDC",
|
||||
"spread": "Spread",
|
||||
"stats": "Stats",
|
||||
"stop-limit": "Stop Limit",
|
||||
|
|
|
@ -94,6 +94,8 @@
|
|||
"current-stats": "Estadísticas actuales",
|
||||
"custom": "Personalizada",
|
||||
"daily-change": "Cambio diario",
|
||||
"daily-high": "24hr High",
|
||||
"daily-low": "24hr Low",
|
||||
"daily-range": "Rango diario",
|
||||
"daily-volume": "Volumen de 24 horas",
|
||||
"dark": "Oscura",
|
||||
|
@ -135,6 +137,8 @@
|
|||
"export-data": "Export CSV",
|
||||
"export-data-empty": "No data to export",
|
||||
"export-data-success": "CSV exported successfully",
|
||||
"favorite": "Favorite",
|
||||
"favorites": "Favorites",
|
||||
"fee": "Tarifa",
|
||||
"fee-discount": "comisiones",
|
||||
"first-deposit-desc": "Necesita 0.035 SOL para crear una cuenta de mango.",
|
||||
|
@ -243,6 +247,7 @@
|
|||
"no-history": "Sin historial comercial",
|
||||
"no-interest": "Sin intereses ganados / pagados",
|
||||
"no-margin": "No se encontraron cuentas de margen",
|
||||
"no-markets": "No markets found",
|
||||
"no-orders": "No hay órdenes abiertas",
|
||||
"no-perp": "No hay puestos de delincuentes",
|
||||
"no-unsettled": "No hay fondos pendientes",
|
||||
|
@ -263,6 +268,7 @@
|
|||
"performance-insights": "Performance Insights",
|
||||
"period-progress": "Period Progress",
|
||||
"perp": "perpetuo",
|
||||
"perp-desc": "Perpetual swaps settled in USDC",
|
||||
"perp-fees": "Tarifas de Mango Perp",
|
||||
"perp-positions": "Posiciones perpetuas",
|
||||
"perp-positions-tip-desc": "Las posiciones de perp acumulan PnL sin liquidar a medida que se mueve el precio. La liquidación de PnL agrega o elimina esa cantidad de su saldo en USDC.",
|
||||
|
@ -328,6 +334,7 @@
|
|||
"slippage-warning": "This order will likely have extremely large slippage! Consider using Stop Limit or Take Profit Limit order instead.",
|
||||
"spanish": "Español",
|
||||
"spot": "Spot",
|
||||
"spot-desc": "Spot margin quoted in USDC",
|
||||
"spread": "Propago",
|
||||
"stats": "Estadisticas",
|
||||
"stop-limit": "Límite de parada",
|
||||
|
|
|
@ -94,6 +94,8 @@
|
|||
"current-stats": "当前统计",
|
||||
"custom": "自定义",
|
||||
"daily-change": "24小时变动",
|
||||
"daily-high": "24hr High",
|
||||
"daily-low": "24hr Low",
|
||||
"daily-range": "24小时广度",
|
||||
"daily-volume": "24小时成交量",
|
||||
"dark": "黑暗",
|
||||
|
@ -135,6 +137,8 @@
|
|||
"export-data": "导出CSV",
|
||||
"export-data-empty": "无资料可导出",
|
||||
"export-data-success": "CSV导出成功",
|
||||
"favorite": "Favorite",
|
||||
"favorites": "Favorites",
|
||||
"fee": "费率",
|
||||
"fee-discount": "费率折扣",
|
||||
"first-deposit-desc": "创建Mango帐户最少需要0.035 SOL。",
|
||||
|
@ -243,6 +247,7 @@
|
|||
"no-history": "您没有交易纪录",
|
||||
"no-interest": "您未收/付过利息",
|
||||
"no-margin": "查不到保证金帐户",
|
||||
"no-markets": "No markets found",
|
||||
"no-orders": "您没有订单",
|
||||
"no-perp": "您没有永续合约持仓",
|
||||
"no-unsettled": "您没有未结清金额",
|
||||
|
@ -263,6 +268,7 @@
|
|||
"performance-insights": "表现分析",
|
||||
"period-progress": "期间进度",
|
||||
"perp": "Perp",
|
||||
"perp-desc": "Perpetual swaps settled in USDC",
|
||||
"perp-fees": "Mango永续合约费率",
|
||||
"perp-positions": "合约当前持仓",
|
||||
"perp-positions-tip-desc": "永续合约当前持仓随着价格波动而累积未结清盈亏。结清盈亏会给您的USDC余额增加或减少。",
|
||||
|
@ -327,6 +333,7 @@
|
|||
"size": "数量",
|
||||
"slippage-warning": "此订单也许会遭受大量滑点!使用限价止损或限价止盈可能比较适合。",
|
||||
"spanish": "Español",
|
||||
"spot-desc": "Spot margin quoted in USDC",
|
||||
"spot": "现货",
|
||||
"spread": "点差",
|
||||
"stats": "统计",
|
||||
|
|
|
@ -94,6 +94,8 @@
|
|||
"current-stats": "當前統計",
|
||||
"custom": "自定義",
|
||||
"daily-change": "24小時變動",
|
||||
"daily-high": "24hr High",
|
||||
"daily-low": "24hr Low",
|
||||
"daily-range": "24小時廣度",
|
||||
"daily-volume": "24小時成交量",
|
||||
"dark": "黑暗",
|
||||
|
@ -135,6 +137,8 @@
|
|||
"export-data": "導出CSV",
|
||||
"export-data-empty": "無資料可導出",
|
||||
"export-data-success": "CSV導出成功",
|
||||
"favorite": "Favorite",
|
||||
"favorites": "Favorites",
|
||||
"fee": "費率",
|
||||
"fee-discount": "費率折扣",
|
||||
"first-deposit-desc": "創建Mango帳戶最少需要0.035 SOL。",
|
||||
|
@ -243,6 +247,7 @@
|
|||
"no-history": "您沒有交易紀錄",
|
||||
"no-interest": "您未收/付過利息",
|
||||
"no-margin": "查不到保證金帳戶",
|
||||
"no-markets": "No markets found",
|
||||
"no-orders": "您沒有訂單",
|
||||
"no-perp": "您沒有永續合約持倉",
|
||||
"no-unsettled": "您沒有未結清金額",
|
||||
|
@ -263,6 +268,7 @@
|
|||
"performance-insights": "表現分析",
|
||||
"period-progress": "期間進度",
|
||||
"perp": "Perp",
|
||||
"perp-desc": "Perpetual swaps settled in USDC",
|
||||
"perp-fees": "Mango永續合約費率",
|
||||
"perp-positions": "合約當前持倉",
|
||||
"perp-positions-tip-desc": "永續合約當前持倉隨著價格波動而累積未結清盈虧。結清盈虧會給您的USDC餘額增加或減少。",
|
||||
|
@ -327,6 +333,7 @@
|
|||
"size": "數量",
|
||||
"slippage-warning": "此訂單也許會遭受大量滑點!使用限價止損或限價止盈可能比較適合。",
|
||||
"spanish": "Español",
|
||||
"spot-desc": "Spot margin quoted in USDC",
|
||||
"spot": "現貨",
|
||||
"spread": "點差",
|
||||
"stats": "統計",
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
} from '@blockworks-foundation/mango-client'
|
||||
import { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js'
|
||||
import { EndpointInfo, WalletAdapter } from '../@types/types'
|
||||
import { isDefined, zipDict } from '../utils'
|
||||
import { isDefined, patchInternalMarketName, zipDict } from '../utils'
|
||||
import { Notification, notify } from '../utils/notifications'
|
||||
import { LAST_ACCOUNT_KEY } from '../components/AccountsModal'
|
||||
import {
|
||||
|
@ -78,6 +78,8 @@ export const programId = new PublicKey(defaultMangoGroupIds.mangoProgramId)
|
|||
export const serumProgramId = new PublicKey(defaultMangoGroupIds.serumProgramId)
|
||||
const mangoGroupPk = new PublicKey(defaultMangoGroupIds.publicKey)
|
||||
|
||||
export const SECONDS = 1000
|
||||
|
||||
// Used to retry loading the MangoGroup and MangoAccount if an rpc node error occurs
|
||||
let mangoGroupRetryAttempt = 0
|
||||
let mangoAccountRetryAttempt = 0
|
||||
|
@ -215,6 +217,7 @@ export interface MangoStore extends State {
|
|||
submitting: boolean
|
||||
success: string
|
||||
}
|
||||
marketInfo: any[]
|
||||
}
|
||||
|
||||
const useMangoStore = create<MangoStore>((set, get) => {
|
||||
|
@ -230,6 +233,7 @@ const useMangoStore = create<MangoStore>((set, get) => {
|
|||
|
||||
const connection = new Connection(rpcUrl, 'processed' as Commitment)
|
||||
return {
|
||||
marketInfo: [],
|
||||
notificationIdCounter: 0,
|
||||
notifications: [],
|
||||
accountInfos: {},
|
||||
|
@ -852,6 +856,24 @@ const useMangoStore = create<MangoStore>((set, get) => {
|
|||
})
|
||||
}
|
||||
},
|
||||
async fetchMarketInfo(markets) {
|
||||
const set = get().set
|
||||
const marketInfos = []
|
||||
await Promise.all(
|
||||
markets.map(async (market) => {
|
||||
const response = await fetch(
|
||||
`https://event-history-api-candles.herokuapp.com/markets/${patchInternalMarketName(
|
||||
market.name
|
||||
)}`
|
||||
)
|
||||
const parsedResponse = await response.json()
|
||||
marketInfos.push(parsedResponse)
|
||||
})
|
||||
)
|
||||
set((state) => {
|
||||
state.marketInfo = marketInfos
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -74,10 +74,18 @@ h1 {
|
|||
@apply font-bold text-th-fgd-1 text-xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply font-bold text-th-fgd-1 text-lg;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply font-bold text-th-fgd-1 text-base;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply font-bold text-th-fgd-3 text-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-sm text-th-fgd-3 mb-2;
|
||||
}
|
||||
|
@ -196,21 +204,23 @@ body::-webkit-scrollbar-corner {
|
|||
|
||||
.thin-scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
background-color: var(--bkg-2);
|
||||
}
|
||||
|
||||
.thin-scroll::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: var(--bkg-3);
|
||||
}
|
||||
|
||||
/* Track */
|
||||
.thin-scroll::-webkit-scrollbar-track {
|
||||
background-color: inherit;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
-webkit-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.thin-scroll::-webkit-scrollbar-corner {
|
||||
background-color: var(--bkg-3);
|
||||
/* Handle */
|
||||
.thin-scroll::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
background: var(--bkg-4);
|
||||
}
|
||||
.thin-scroll::-webkit-scrollbar-thumb:window-inactive {
|
||||
background: var(--bkg-4);
|
||||
}
|
||||
|
||||
/* Responsive table */
|
||||
|
|
Loading…
Reference in New Issue