Merge pull request #21 from blockworks-foundation/token-details

add token details page
This commit is contained in:
tylersssss 2022-10-28 12:50:39 -04:00 committed by GitHub
commit 08c16757ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 932 additions and 39 deletions

View File

@ -75,12 +75,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
isCollapsed ? 'md:pl-[64px]' : 'md:pl-44 lg:pl-48 xl:pl-52'
}`}
>
<div className="flex h-16 items-center justify-between border-b border-th-bkg-3 bg-th-bkg-1 pl-6">
<img
className="mr-4 h-8 w-auto md:hidden"
src="/logos/logo-mark.svg"
alt="next"
/>
<div className="flex h-16 items-center justify-between border-b border-th-bkg-3 bg-th-bkg-1 pl-4 md:pl-6">
<TopBar />
</div>
{children}

View File

@ -2,6 +2,7 @@ import { Bank, MangoAccount } from '@blockworks-foundation/mango-v4'
import { Transition } from '@headlessui/react'
import {
ChevronDownIcon,
ChevronRightIcon,
EllipsisHorizontalIcon,
QuestionMarkCircleIcon,
} from '@heroicons/react/20/solid'
@ -11,7 +12,6 @@ import Image from "next/legacy/image";
import { useRouter } from 'next/router'
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
import { useViewport } from '../hooks/useViewport'
import mangoStore from '@store/mangoStore'
import { formatDecimal, formatFixedDecimals } from '../utils/numbers'
import { breakpoints } from '../utils/theme'
@ -38,6 +38,7 @@ const TokenList = () => {
)
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const router = useRouter()
const banks = useMemo(() => {
if (group) {
@ -74,6 +75,10 @@ const TokenList = () => {
}
}, [connected])
const goToTokenPage = (bank: Bank) => {
router.push(`/token/${bank.name}`, undefined, { shallow: true })
}
return (
<ContentBox hideBorder hidePadding className="md:-mt-[36px]">
<div className="flex items-center justify-end md:mb-5">
@ -226,6 +231,9 @@ const TokenList = () => {
id={i === 0 ? 'account-step-ten' : ''}
>
<ActionsMenu bank={bank} mangoAccount={mangoAccount} />
<IconButton onClick={() => goToTokenPage(bank)}>
<ChevronRightIcon className="h-5 w-5" />
</IconButton>
</div>
</td>
</tr>
@ -247,7 +255,7 @@ const TokenList = () => {
export default TokenList
const MobileTokenListItem = ({ bank }: { bank: Bank }) => {
const { t } = useTranslation('common')
const { t } = useTranslation(['common', 'token'])
const [showTokenDetails, setShowTokenDetails] = useState(false)
const jupiterTokens = mangoStore((s) => s.jupiterTokens)
const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances)
@ -257,6 +265,7 @@ const MobileTokenListItem = ({ bank }: { bank: Bank }) => {
)
const symbol = bank.name
const oraclePrice = bank.uiPrice
const router = useRouter()
let logoURI
if (jupiterTokens.length) {
@ -282,6 +291,10 @@ const MobileTokenListItem = ({ bank }: { bank: Bank }) => {
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0.0
const goToTokenPage = (bank: Bank) => {
router.push(`/token/${bank.name}`, undefined, { shallow: true })
}
return (
<div key={symbol} className="border-b border-th-bkg-3 px-6 py-4">
<div className="flex items-center justify-between">
@ -367,6 +380,15 @@ const MobileTokenListItem = ({ bank }: { bank: Bank }) => {
</span>
</p>
</div>
<div className="col-span-1">
<LinkButton
className="flex items-center"
onClick={() => goToTokenPage(bank)}
>
{t('token:token-details')}
<ChevronRightIcon className="ml-2 h-5 w-5" />
</LinkButton>
</div>
</div>
</Transition>
</div>
@ -386,7 +408,7 @@ const ActionsMenu = ({
const [showBorrowModal, setShowBorrowModal] = useState(false)
const [selectedToken, setSelectedToken] = useState('')
// const set = mangoStore.getState().set
const router = useRouter()
// const router = useRouter()
// const { asPath } = router
const jupiterTokens = mangoStore((s) => s.jupiterTokens)

View File

@ -1,11 +1,10 @@
import { useCallback, useState } from 'react'
import { ArrowRightIcon } from '@heroicons/react/20/solid'
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
import mangoStore from '@store/mangoStore'
import WalletIcon from './icons/WalletIcon'
import MangoAccountsList from './MangoAccountsList'
import { LinkButton } from './shared/Button'
import { IconButton, LinkButton } from './shared/Button'
import ConnectedMenu from './wallet/ConnectedMenu'
import { ConnectWalletButton } from './wallet/ConnectWalletButton'
import { IS_ONBOARDED_KEY } from '../utils/constants'
@ -13,6 +12,7 @@ import useLocalStorageState from '../hooks/useLocalStorageState'
import UserSetupModal from './modals/UserSetupModal'
import CreateAccountModal from './modals/CreateAccountModal'
import MangoAccountsListModal from './modals/MangoAccountsListModal'
import { useRouter } from 'next/router'
const TopBar = () => {
const { t } = useTranslation('common')
@ -22,6 +22,8 @@ const TopBar = () => {
const [showUserSetupModal, setShowUserSetupModal] = useState(false)
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
const [showMangoAccountsModal, setShowMangoAccountsModal] = useState(false)
const router = useRouter()
const { query } = router
const handleCloseModal = useCallback(() => {
setShowUserSetupModal(false)
@ -34,7 +36,23 @@ const TopBar = () => {
return (
<>
<div className="flex w-full items-center justify-between space-x-4">
<span className="mb-0">
<span className="mb-0 flex items-center">
{query.token ? (
<div
className={`mr-2 flex h-16 items-center pr-4 md:mr-4 md:pr-6 ${
!connected || !mangoAccount ? 'border-r border-th-bkg-3' : ''
}`}
>
<IconButton onClick={() => router.back()} hideBg size="small">
<ArrowLeftIcon className="h-6 w-6" />
</IconButton>
</div>
) : null}
<img
className="mr-4 ml-2 h-8 w-auto md:hidden"
src="/logos/logo-mark.svg"
alt="next"
/>
{!connected ? (
<span className="hidden items-center md:flex">
<WalletIcon className="h-5 w-5 text-th-fgd-3" />
@ -52,9 +70,8 @@ const TopBar = () => {
</span>
{connected ? (
<div className="flex items-center space-x-4 pr-4 md:pr-0">
{/* <MangoAccountsList mangoAccount={mangoAccount} /> */}
<button
className="mr-2"
className="mr-2 hidden md:block"
onClick={() => setShowMangoAccountsModal(true)}
>
<p className="text-right text-xs">{t('accounts')}</p>
@ -80,7 +97,6 @@ const TopBar = () => {
<MangoAccountsListModal
isOpen={showMangoAccountsModal}
onClose={() => setShowMangoAccountsModal(false)}
mangoAccount={mangoAccount}
/>
) : null}
{showUserSetupModal ? (

View File

@ -18,15 +18,16 @@ import CreateAccountForm from '@components/account/CreateAccountForm'
import { EnterRightExitLeft } from '@components/shared/Transitions'
const MangoAccountsListModal = ({
mangoAccount,
// mangoAccount,
isOpen,
onClose,
}: {
mangoAccount: MangoAccount | undefined
// mangoAccount: MangoAccount | undefined
isOpen: boolean
onClose: () => void
}) => {
const { t } = useTranslation('common')
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const mangoAccounts = mangoStore((s) => s.mangoAccounts)
const actions = mangoStore((s) => s.actions)
const group = mangoStore((s) => s.group)

View File

@ -0,0 +1,37 @@
import { useMemo } from 'react'
import { formatFixedDecimals } from 'utils/numbers'
interface DailyRangeProps {
high: number
low: number
price: number
}
const DailyRange = ({ high, low, price }: DailyRangeProps) => {
const rangePercent = useMemo(() => {
return ((price - low) * 100) / (high - low)
}, [high, low, price])
return (
<div className="flex items-center justify-between md:block">
<div className="flex items-center">
<span className={`pr-2 font-mono text-th-fgd-2`}>
{formatFixedDecimals(low, true)}
</span>
<div className="mt-[2px] flex h-2 w-32 rounded-sm bg-th-bkg-3">
<div
style={{
width: `${rangePercent}%`,
}}
className="flex rounded-sm bg-th-primary"
></div>
</div>
<span className={`pl-2 font-mono text-th-fgd-2`}>
{formatFixedDecimals(high, true)}
</span>
</div>
</div>
)
}
export default DailyRange

View File

@ -1,4 +1,4 @@
import { FunctionComponent, ReactNode, useMemo, useState } from 'react'
import { FunctionComponent, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import {
@ -13,7 +13,6 @@ import FlipNumbers from 'react-flip-numbers'
import LineChartIcon from '../icons/LineChartIcon'
import ContentBox from '../shared/ContentBox'
import { DownTriangle, UpTriangle } from '../shared/DirectionTriangles'
import SheenLoader from '../shared/SheenLoader'
import { COLORS } from '../../styles/colors'
import { useTheme } from 'next-themes'
@ -38,6 +37,14 @@ interface DetailedAreaChartProps {
yKey: string
}
export const formatDateAxis = (date: string, days: number) => {
if (days === 1) {
return dayjs(date).format('h:mma')
} else {
return dayjs(date).format('D MMM')
}
}
const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
data,
daysToShow = 1,
@ -80,14 +87,6 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
return 0
}
const formatDateAxis = (date: string) => {
if (daysToShow === 1) {
return dayjs(date).format('h:mma')
} else {
return dayjs(date).format('D MMM')
}
}
const flipGradientCoords = useMemo(
() => data[0][yKey] <= 0 && data[data.length - 1][yKey] < data[0][yKey],
[data]
@ -229,7 +228,7 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
fontSize: 10,
}}
tickLine={false}
tickFormatter={(d) => formatDateAxis(d)}
tickFormatter={(d) => formatDateAxis(d, daysToShow)}
/>
<YAxis
axisLine={false}

View File

@ -1,6 +1,7 @@
import { Transition } from '@headlessui/react'
import {
ChevronDownIcon,
ChevronRightIcon,
QuestionMarkCircleIcon,
} from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
@ -11,18 +12,21 @@ import { useViewport } from '../../hooks/useViewport'
import mangoStore from '@store/mangoStore'
import { formatDecimal, formatFixedDecimals } from '../../utils/numbers'
import { breakpoints } from '../../utils/theme'
import { IconButton } from '../shared/Button'
import { IconButton, LinkButton } from '../shared/Button'
import ContentBox from '../shared/ContentBox'
import FlipNumbers from 'react-flip-numbers'
import Tooltip from '@components/shared/Tooltip'
import { Bank } from '@blockworks-foundation/mango-v4'
import { useRouter } from 'next/router'
const TokenList = () => {
const { t } = useTranslation('common')
const TokenStats = () => {
const { t } = useTranslation(['common', 'token'])
const [showTokenDetails, setShowTokenDetails] = useState('')
const group = mangoStore((s) => s.group)
const jupiterTokens = mangoStore((s) => s.jupiterTokens)
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const router = useRouter()
const banks = useMemo(() => {
if (group) {
@ -55,6 +59,10 @@ const TokenList = () => {
return []
}, [banks])
const goToTokenPage = (bank: Bank) => {
router.push(`/token/${bank.name}`, undefined, { shallow: true })
}
return (
<ContentBox hideBorder hidePadding>
<div className="grid grid-cols-2 gap-x-6 border-b border-th-bkg-3 text-[40px]">
@ -113,7 +121,7 @@ const TokenList = () => {
</div>
</th>
<th>
<div className="flex justify-end">
<div className="flex justify-end text-right">
<Tooltip content={t('asset-weight-desc')}>
<span className="tooltip-underline">
{t('asset-weight')}
@ -204,6 +212,13 @@ const TokenList = () => {
<p>{bank.initLiabWeight.toFixed(2)}</p>
</div>
</td>
<td>
<div className="flex justify-end">
<IconButton onClick={() => goToTokenPage(bank)}>
<ChevronRightIcon className="h-5 w-5" />
</IconButton>
</div>
</td>
</tr>
)
})}
@ -315,6 +330,15 @@ const TokenList = () => {
{bank.initLiabWeight.toFixed(2)}
</p>
</div>
<div className="col-span-1">
<LinkButton
className="flex items-center"
onClick={() => goToTokenPage(bank)}
>
{t('token:token-details')}
<ChevronRightIcon className="ml-2 h-5 w-5" />
</LinkButton>
</div>
</div>
</Transition>
</div>
@ -326,4 +350,4 @@ const TokenList = () => {
)
}
export default TokenList
export default TokenStats

View File

@ -0,0 +1,88 @@
import { formatDateAxis } from '@components/shared/DetailedAreaChart'
import { useTheme } from 'next-themes'
import { useMemo } from 'react'
import { Area, AreaChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'
import { COLORS } from 'styles/colors'
const PriceChart = ({
prices,
daysToShow,
}: {
prices: number[][]
daysToShow: number
}) => {
const { theme } = useTheme()
const change = useMemo(() => {
return prices[prices.length - 1][1] - prices[0][1]
}, [prices])
return (
<div className="relative -mt-1 h-96 w-auto">
<div className="-mx-6 mt-6 h-full px-10">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={prices}>
<defs>
<linearGradient id="gradientArea" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={
change >= 0 ? COLORS.GREEN[theme] : COLORS.RED[theme]
}
stopOpacity={0.15}
/>
<stop
offset="99%"
stopColor={
change >= 0 ? COLORS.GREEN[theme] : COLORS.RED[theme]
}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
isAnimationActive={false}
type="monotone"
dataKey="1"
stroke={change >= 0 ? COLORS.GREEN[theme] : COLORS.RED[theme]}
strokeWidth={1.5}
fill="url(#gradientArea)"
/>
<XAxis
axisLine={false}
dataKey="0"
padding={{ left: 20, right: 20 }}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.6)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(d) => formatDateAxis(d, daysToShow)}
/>
<YAxis
axisLine={false}
dataKey={'1'}
type="number"
domain={['dataMin', 'dataMax']}
padding={{ top: 20, bottom: 20 }}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.6)',
fontSize: 10,
}}
tickFormatter={(x) => `$${x.toFixed(2)}`}
tickLine={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
)
}
export default PriceChart

View File

@ -1,6 +1,7 @@
import { Menu, Transition } from '@headlessui/react'
import {
ArrowRightOnRectangleIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
@ -14,10 +15,12 @@ import { PublicKey } from '@solana/web3.js'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from '../../utils/theme'
import EditProfileModal from '@components/modals/EditProfileModal'
import MangoAccountsListModal from '@components/modals/MangoAccountsListModal'
const ConnectedMenu = () => {
const { t } = useTranslation('common')
const [showEditProfileModal, setShowEditProfileModal] = useState(false)
const [showMangoAccountsModal, setShowMangoAccountsModal] = useState(false)
const set = mangoStore((s) => s.set)
const { publicKey, disconnect, wallet } = useWallet()
const actions = mangoStore((s) => s.actions)
@ -106,15 +109,17 @@ const ConnectedMenu = () => {
</div>
</button>
</Menu.Item>
{/* <Menu.Item>
{isMobile ? (
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => setShowAccountsModal(true)}
onClick={() => setShowMangoAccountsModal(true)}
>
<CurrencyDollarIcon className="h-4 w-4" />
<div className="pl-2 text-left">{t('accounts')}</div>
</button>
</Menu.Item> */}
</Menu.Item>
) : null}
{/* <Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
@ -153,6 +158,13 @@ const ConnectedMenu = () => {
onClose={() => setShowEditProfileModal(false)}
/>
) : null}
{showMangoAccountsModal ? (
<MangoAccountsListModal
isOpen={showMangoAccountsModal}
onClose={() => setShowMangoAccountsModal(false)}
/>
) : null}
</>
)
}

View File

@ -29,6 +29,7 @@
"date-fns": "^2.29.3",
"dayjs": "^1.11.3",
"decimal.js": "^10.4.0",
"html-react-parser": "^3.0.4",
"immer": "^9.0.12",
"jsbi": "^4.3.0",
"lodash": "^4.17.21",

View File

@ -5,7 +5,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'profile'])),
...(await serverSideTranslations(locale, ['common', 'profile', 'token'])),
},
}
}

529
pages/token/[token].tsx Normal file
View File

@ -0,0 +1,529 @@
import Change from '@components/shared/Change'
import DailyRange from '@components/shared/DailyRange'
import mangoStore from '@store/mangoStore'
import type { NextPage } from 'next'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import FlipNumbers from 'react-flip-numbers'
import { formatDecimal, formatFixedDecimals } from 'utils/numbers'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import Button from '@components/shared/Button'
import { ArrowSmallUpIcon } from '@heroicons/react/20/solid'
import DepositModal from '@components/modals/DepositModal'
import BorrowModal from '@components/modals/BorrowModal'
import parse from 'html-react-parser'
import Link from 'next/link'
import SheenLoader from '@components/shared/SheenLoader'
import Tooltip from '@components/shared/Tooltip'
import ChartRangeButtons from '@components/shared/ChartRangeButtons'
import dynamic from 'next/dynamic'
import { LISTED_TOKENS } from 'utils/tokens'
const PriceChart = dynamic(() => import('@components/token/PriceChart'), {
ssr: false,
})
dayjs.extend(relativeTime)
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'profile', 'token'])),
},
}
}
export const getStaticPaths = async () => {
const paths = LISTED_TOKENS.map((token) => ({
params: { token: token },
}))
return { paths, fallback: false }
}
const DEFAULT_COINGECKO_VALUES = {
ath: 0,
atl: 0,
ath_change_percentage: 0,
atl_change_percentage: 0,
ath_date: 0,
atl_date: 0,
high_24h: 0,
circulating_supply: 0,
fully_diluted_valuation: 0,
low_24h: 0,
market_cap: 0,
max_supply: 0,
price_change_percentage_24h: 0,
total_supply: 0,
total_volume: 0,
}
const Token: NextPage = () => {
const { t } = useTranslation(['common', 'token'])
const [showFullDesc, setShowFullDesc] = useState(false)
const [showDepositModal, setShowDepositModal] = useState(false)
const [showBorrowModal, setShowBorrowModal] = useState(false)
const [coingeckoData, setCoingeckoData] = useState<any>(null)
const [loading, setLoading] = useState(true)
const router = useRouter()
const { token } = router.query
const group = mangoStore((s) => s.group)
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const jupiterTokens = mangoStore((s) => s.jupiterTokens)
const coingeckoPrices = mangoStore((s) => s.coingeckoPrices.data)
const [chartData, setChartData] = useState<{ prices: any[] } | null>(null)
const [loadChartData, setLoadChartData] = useState(true)
const loadingCoingeckoPrices = mangoStore((s) => s.coingeckoPrices.loading)
const [daysToShow, setDaysToShow] = useState<number>(1)
const bank = useMemo(() => {
if (group && token) {
const bank = group.banksMapByName.get(token.toString())
if (bank) {
return bank[0]
} else {
setLoading(false)
}
}
}, [group, token])
const logoURI = useMemo(() => {
if (bank && jupiterTokens.length) {
return jupiterTokens.find((t) => t.address === bank.mint.toString())
?.logoURI
}
}, [bank, jupiterTokens])
const coingeckoId = useMemo(() => {
if (bank && jupiterTokens.length) {
return jupiterTokens.find((t) => t.address === bank.mint.toString())
?.extensions?.coingeckoId
}
}, [bank, jupiterTokens])
const serumMarkets = useMemo(() => {
if (group) {
return Array.from(group.serum3MarketsMapByExternal.values())
}
return []
}, [group])
const handleTrade = () => {
const set = mangoStore.getState().set
const market = serumMarkets.find(
(m) => m.baseTokenIndex === bank?.tokenIndex
)
if (market) {
set((state) => {
state.selectedMarket.current = market
})
router.push('/trade')
}
}
const fetchTokenInfo = async (tokenId: string) => {
const response = await fetch(
`https://api.coingecko.com/api/v3/coins/${tokenId}?localization=false&tickers=false&developer_data=false&sparkline=false
`
)
const data = await response.json()
return data
}
const getCoingeckoData = async (id: string) => {
const response = await fetchTokenInfo(id)
setCoingeckoData(response)
setLoading(false)
}
useEffect(() => {
if (coingeckoId) {
getCoingeckoData(coingeckoId)
}
}, [coingeckoId])
const {
ath,
atl,
ath_change_percentage,
atl_change_percentage,
ath_date,
atl_date,
high_24h,
circulating_supply,
fully_diluted_valuation,
low_24h,
market_cap,
max_supply,
price_change_percentage_24h,
total_supply,
total_volume,
} = coingeckoData ? coingeckoData.market_data : DEFAULT_COINGECKO_VALUES
const loadingChart = useMemo(() => {
return daysToShow == 1 ? loadingCoingeckoPrices : loadChartData
}, [loadChartData, loadingCoingeckoPrices])
const coingeckoTokenPrices = useMemo(() => {
if (daysToShow === 1 && coingeckoPrices.length && bank) {
const tokenPriceData = coingeckoPrices.find((asset) =>
bank?.name === 'soETH'
? asset.symbol === 'ETH'
: asset.symbol === bank.name
)
if (tokenPriceData) {
return tokenPriceData.prices
}
} else {
if (chartData && !loadingChart) {
return chartData.prices
}
}
return []
}, [coingeckoPrices, bank, daysToShow, chartData, loadingChart])
const handleDaysToShow = async (days: number) => {
if (days !== 1) {
try {
const response = await fetch(
`https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`
)
const data = await response.json()
setLoadChartData(false)
setChartData(data)
} catch {
setLoadChartData(false)
}
}
setDaysToShow(days)
}
return (
<div className="pb-20 md:pb-16">
{coingeckoData && bank ? (
<>
<div className="flex flex-col border-b border-th-bkg-3 px-6 py-3 md:flex-row md:items-center md:justify-between">
<div className="mb-4 md:mb-1">
<div className="mb-1.5 flex items-center space-x-2">
<Image src={logoURI!} height="20" width="20" />
<h1 className="text-base font-normal">
{coingeckoData.name}{' '}
<span className="text-th-fgd-4">({bank.name})</span>
</h1>
</div>
<div className="mb-2 flex items-end space-x-3 text-5xl font-bold text-th-fgd-1">
$
<FlipNumbers
height={48}
width={32}
play
delay={0.05}
duration={1}
numbers={formatDecimal(bank.uiPrice, 2)}
/>
<Change change={price_change_percentage_24h} />
</div>
<DailyRange
high={high_24h.usd}
low={low_24h.usd}
price={bank.uiPrice}
/>
</div>
<div className="w-full rounded-md bg-th-bkg-2 p-4 md:w-[343px]">
<div className="mb-4 flex justify-between">
<p>
{bank.name} {t('balance')}:
</p>
<p className="font-mono text-th-fgd-2">
{mangoAccount
? formatDecimal(
mangoAccount.getTokenBalanceUi(bank),
bank.mintDecimals
)
: 0}
</p>
</div>
<div className="flex space-x-2">
<Button
className="flex-1"
size="small"
disabled={!mangoAccount}
onClick={() => setShowDepositModal(true)}
>
{t('deposit')}
</Button>
<Button
className="flex-1"
size="small"
secondary
disabled={!mangoAccount}
onClick={() => setShowBorrowModal(true)}
>
{t('borrow')}
</Button>
<Button
className="flex-1"
size="small"
secondary
disabled={
!mangoAccount ||
!serumMarkets.find(
(m) => m.baseTokenIndex === bank?.tokenIndex
)
}
onClick={handleTrade}
>
{t('trade')}
</Button>
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2">
<div className="col-span-1 border-b border-r border-th-bkg-3 px-6 py-4 sm:border-b-0">
<h2 className="mb-4 text-base">{t('token:lending')}</h2>
<div className="flex justify-between border-t border-th-bkg-3 py-4">
<p>{t('total-deposits')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(bank.uiDeposits())}
</p>
</div>
<div className="flex justify-between border-t border-th-bkg-3 py-4">
<p>{t('token:total-value')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(bank.uiDeposits() * bank.uiPrice, true)}
</p>
</div>
<div className="flex justify-between border-t border-th-bkg-3 pt-4">
<p>{t('deposit-rate')}</p>
<p className="font-mono text-th-green">
{formatDecimal(bank.getDepositRateUi(), 2, {
fixed: true,
})}
%
</p>
</div>
</div>
<div className="col-span-1 px-6 py-4">
<h2 className="mb-4 text-base">{t('token:borrowing')}</h2>
<div className="flex justify-between border-t border-th-bkg-3 py-4">
<p>{t('total-borrows')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(bank.uiBorrows())}
</p>
</div>
<div className="flex justify-between border-t border-th-bkg-3 py-4">
<p>{t('token:total-value')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(bank.uiBorrows() * bank.uiPrice, true)}
</p>
</div>
<div className="flex justify-between border-t border-th-bkg-3 pt-4">
<p>{t('borrow-rate')}</p>
<p className="font-mono text-th-red">
{formatDecimal(bank.getBorrowRateUi(), 2, {
fixed: true,
})}
%
</p>
</div>
</div>
</div>
<div className="flex items-center justify-center border-y border-th-bkg-3 px-6 py-4 text-center">
<Tooltip
content={'The percentage of deposits that have been lent out.'}
>
<p className="tooltip-underline mr-1">{t('utilization')}:</p>
</Tooltip>
<span className="font-mono text-th-fgd-2 no-underline">
{bank.uiDeposits() > 0
? formatDecimal(
(bank.uiBorrows() / bank.uiDeposits()) * 100,
1,
{ fixed: true }
)
: '0.0'}
%
</span>
</div>
<div className="border-b border-th-bkg-3 py-4 px-6">
<h2 className="mb-1 text-xl">About {bank.name}</h2>
<div className="flex items-end">
<p
className={`${
showFullDesc ? 'h-full' : 'h-5'
} max-w-[720px] overflow-hidden`}
>
{parse(coingeckoData.description.en)}
</p>
<span
className="default-transition flex cursor-pointer items-end font-normal underline hover:text-th-fgd-2 md:hover:no-underline"
onClick={() => setShowFullDesc(!showFullDesc)}
>
{showFullDesc ? 'Less' : 'More'}
<ArrowSmallUpIcon
className={`h-5 w-5 ${
showFullDesc ? 'rotate-360' : 'rotate-180'
} default-transition`}
/>
</span>
</div>
</div>
{!loadingChart ? (
coingeckoTokenPrices.length ? (
<>
<div className="mt-4 flex w-full items-center justify-between px-6">
<h2 className="text-base">{bank.name} Price Chart</h2>
<ChartRangeButtons
activeValue={daysToShow}
names={['24H', '7D', '30D']}
values={[1, 7, 30]}
onChange={(v) => handleDaysToShow(v)}
/>
</div>
<PriceChart
daysToShow={daysToShow}
prices={coingeckoTokenPrices}
/>
</>
) : bank?.name === 'USDC' || bank?.name === 'USDT' ? null : (
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
)
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
<div className="grid grid-cols-1 border-b border-th-bkg-3 sm:grid-cols-2">
<div className="col-span-1 border-y border-th-bkg-3 px-6 py-4 sm:col-span-2">
<h2 className="text-base">{bank.name} Stats</h2>
</div>
<div className="col-span-1 border-r border-th-bkg-3 px-6 py-4">
<div className="flex justify-between pb-4">
<p>{t('token:market-cap')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(market_cap.usd, true)}{' '}
<span className="text-th-fgd-4">
#{coingeckoData.market_cap_rank}
</span>
</p>
</div>
<div className="flex justify-between border-t border-th-bkg-3 py-4">
<p>{t('token:volume')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(total_volume.usd, true)}
</p>
</div>
<div className="flex justify-between border-t border-th-bkg-3 py-4">
<p>{t('token:all-time-high')}</p>
<div className="flex flex-col items-end">
<div className="flex items-center font-mono text-th-fgd-2">
<span className="mr-2">
{formatFixedDecimals(ath.usd, true)}
</span>
<Change change={ath_change_percentage.usd} />
</div>
<p className="text-xs text-th-fgd-4">
{dayjs(ath_date.usd).format('MMM, D, YYYY')} (
{dayjs(ath_date.usd).fromNow()})
</p>
</div>
</div>
<div className="flex justify-between border-b border-t border-th-bkg-3 py-4 sm:border-b-0 sm:pb-0">
<p>{t('token:all-time-low')}</p>
<div className="flex flex-col items-end">
<div className="flex items-center font-mono text-th-fgd-2">
<span className="mr-2">
{formatFixedDecimals(atl.usd, true)}
</span>
<Change change={atl_change_percentage.usd} />
</div>
<p className="text-xs text-th-fgd-4">
{dayjs(atl_date.usd).format('MMM, D, YYYY')} (
{dayjs(atl_date.usd).fromNow()})
</p>
</div>
</div>
</div>
<div className="col-span-1 px-6 pb-4 sm:pt-4">
{fully_diluted_valuation.usd ? (
<div className="flex justify-between pb-4">
<p>{t('token:fdv')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(fully_diluted_valuation.usd, true)}
</p>
</div>
) : null}
<div
className={`flex justify-between ${
fully_diluted_valuation.usd
? 'border-t border-th-bkg-3 py-4'
: 'pb-4'
}`}
>
<p>{t('token:circulating-supply')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(circulating_supply)}
</p>
</div>
<div
className={`flex justify-between border-t border-th-bkg-3 ${
max_supply ? 'py-4' : 'border-b pt-4 sm:pb-4'
}`}
>
<p>{t('token:total-supply')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(total_supply)}
</p>
</div>
{max_supply ? (
<div className="flex justify-between border-t border-th-bkg-3 pt-4">
<p>{t('token:max-supply')}</p>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(max_supply)}
</p>
</div>
) : null}
</div>
</div>
</>
) : loading ? (
<div className="space-y-3 px-6 py-4">
<SheenLoader className="flex flex-1">
<div className="h-32 w-full rounded-lg bg-th-bkg-2" />
</SheenLoader>
<SheenLoader className="flex flex-1">
<div className="h-72 w-full rounded-lg bg-th-bkg-2" />
</SheenLoader>
</div>
) : (
<div className="-mt-8 flex h-screen flex-col items-center justify-center">
<p className="text-3xl">😔</p>
<h2 className="mb-1">{t('token:token-not-found')}</h2>
<p className="mb-2">
{t('token:token-not-found-desc', { token: token })}
</p>
<Link href="/">
<a>{t('token:go-to-account')}</a>
</Link>
</div>
)}
{showDepositModal ? (
<DepositModal
isOpen={showDepositModal}
onClose={() => setShowDepositModal(false)}
token={bank!.name}
/>
) : null}
{showBorrowModal ? (
<BorrowModal
isOpen={showBorrowModal}
onClose={() => setShowBorrowModal(false)}
token={bank!.name}
/>
) : null}
</div>
)
}
export default Token

View File

@ -0,0 +1,17 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"borrowing": "Borrowing",
"circulating-supply": "Circulating Supply",
"fdv": "Fully Diluted Value",
"go-to-account": "Go To Account",
"lending": "Lending",
"market-cap": "Market Cap",
"max-supply": "Max Supply",
"token-details": "Token Details",
"token-not-found": "Token Not Found",
"token-not-found-desc": "'{{token}}' is either not listed or we're having issues loading the data.",
"total-supply": "Total Supply",
"total-value": "Total Value",
"volume": "24h Volume"
}

View File

@ -0,0 +1,17 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"borrowing": "Borrowing",
"circulating-supply": "Circulating Supply",
"fdv": "Fully Diluted Value",
"go-to-account": "Go To Account",
"lending": "Lending",
"market-cap": "Market Cap",
"max-supply": "Max Supply",
"token-details": "Token Details",
"token-not-found": "Token Not Found",
"token-not-found-desc": "'{{token}}' is either not listed or we're having issues loading the data.",
"total-supply": "Total Supply",
"total-value": "Total Value",
"volume": "24h Volume"
}

View File

@ -0,0 +1,17 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"borrowing": "Borrowing",
"circulating-supply": "Circulating Supply",
"fdv": "Fully Diluted Value",
"go-to-account": "Go To Account",
"lending": "Lending",
"market-cap": "Market Cap",
"max-supply": "Max Supply",
"token-details": "Token Details",
"token-not-found": "Token Not Found",
"token-not-found-desc": "'{{token}}' is either not listed or we're having issues loading the data.",
"total-supply": "Total Supply",
"total-value": "Total Value",
"volume": "24h Volume"
}

View File

@ -0,0 +1,17 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"borrowing": "Borrowing",
"circulating-supply": "Circulating Supply",
"fdv": "Fully Diluted Value",
"go-to-account": "Go To Account",
"lending": "Lending",
"market-cap": "Market Cap",
"max-supply": "Max Supply",
"token-details": "Token Details",
"token-not-found": "Token Not Found",
"token-not-found-desc": "'{{token}}' is either not listed or we're having issues loading the data.",
"total-supply": "Total Supply",
"total-value": "Total Value",
"volume": "24h Volume"
}

View File

@ -0,0 +1,17 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"borrowing": "Borrowing",
"circulating-supply": "Circulating Supply",
"fdv": "Fully Diluted Value",
"go-to-account": "Go To Account",
"lending": "Lending",
"market-cap": "Market Cap",
"max-supply": "Max Supply",
"token-details": "Token Details",
"token-not-found": "Token Not Found",
"token-not-found-desc": "'{{token}}' is either not listed or we're having issues loading the data.",
"total-supply": "Total Supply",
"total-value": "Total Value",
"volume": "24h Volume"
}

View File

@ -44,7 +44,8 @@ export const COINGECKO_IDS = [
// { id: 'cope', symbol: 'COPE' },
// { id: 'cardano', symbol: 'ADA' },
{ id: 'msol', symbol: 'MSOL' },
// { id: 'tether', symbol: 'USDT' },
{ id: 'usd-coin', symbol: 'USDC' },
{ id: 'tether', symbol: 'USDT' },
// { id: 'stepn', symbol: 'GMT' },
]

View File

@ -105,3 +105,13 @@ export const fetchNftsFromHolaplexIndexer = async (owner: PublicKey) => {
export const formatTokenSymbol = (symbol: string) =>
symbol === 'MSOL' ? 'mSOL' : symbol === 'SOETH' ? 'soETH' : symbol
export const LISTED_TOKENS: string[] = [
'BTC',
'ETH',
'soETH',
'SOL',
'MSOL',
'USDC',
'USDT',
]

View File

@ -3243,6 +3243,36 @@ dom-helpers@^3.4.0:
dependencies:
"@babel/runtime" "^7.1.2"
dom-serializer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.2"
entities "^4.2.0"
domelementtype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domhandler@5.0.3, domhandler@^5.0.1, domhandler@^5.0.2:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
dependencies:
domelementtype "^2.3.0"
domutils@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c"
integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==
dependencies:
dom-serializer "^2.0.0"
domelementtype "^2.3.0"
domhandler "^5.0.1"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
@ -3333,6 +3363,11 @@ engine.io-parser@~5.0.3:
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
entities@^4.2.0, entities@^4.3.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
err-code@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
@ -4191,6 +4226,14 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
dependencies:
react-is "^16.7.0"
html-dom-parser@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-3.1.2.tgz#c137c42df80e17d185ff35a806925d96cc73f408"
integrity sha512-mLTtl3pVn3HnqZSZzW3xVs/mJAKrG1yIw3wlp+9bdoZHHLaBRvELdpfShiPVLyjPypq1Fugv2KMDoGHW4lVXnw==
dependencies:
domhandler "5.0.3"
htmlparser2 "8.0.1"
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
@ -4198,6 +4241,26 @@ html-parse-stringify@^3.0.1:
dependencies:
void-elements "3.1.0"
html-react-parser@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-3.0.4.tgz#6a6a115a011dfdadd901ca9d2ed80fa5390647e5"
integrity sha512-va68PSmC7uA6PbOEc9yuw5Mu3OHPXmFKUpkLGvUPdTuNrZ0CJZk1s/8X/FaHjswK/6uZghu2U02tJjussT8+uw==
dependencies:
domhandler "5.0.3"
html-dom-parser "3.1.2"
react-property "2.0.0"
style-to-js "1.1.1"
htmlparser2@8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010"
integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.2"
domutils "^3.0.1"
entities "^4.3.0"
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
@ -4261,6 +4324,11 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.4:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inline-style-parser@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
internal-slot@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@ -5681,6 +5749,11 @@ react-number-format@^4.9.2:
dependencies:
prop-types "^15.7.2"
react-property@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.0.tgz#2156ba9d85fa4741faf1918b38efc1eae3c6a136"
integrity sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==
react-qr-reader@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-2.2.1.tgz#dc89046d1c1a1da837a683dd970de5926817d55b"