Merge pull request #148 from blockworks-foundation/trade-nav-concept

Trade page navigation concept
This commit is contained in:
tjshipe 2022-02-18 10:48:53 -05:00 committed by GitHub
commit 732c44dd39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 813 additions and 179 deletions

View File

@ -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>
)}

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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' : ''}

View File

@ -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>
)
}

View File

@ -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

View File

@ -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>

229
components/TradeNavMenu.tsx Normal file
View File

@ -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>
)
}

View File

@ -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}

View File

@ -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>

View File

@ -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" />
) : (

View File

@ -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) {

View File

@ -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 && (

View File

@ -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",

View File

@ -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",

View File

@ -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": "统计",

View File

@ -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": "統計",

View File

@ -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
})
},
},
}
})

View File

@ -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 */