Mp 2540 farm overview (#172)

* tidy: refactor text

* tidy: refactor text

* fix display of vaults table

* feat: added unstyled select

* tidy: useToggle

* tidy: useToggle

* add vaults to types

* MP-2344: first unstyled version of Select

* fix: fixed the build

* MP-2344: progress on the Select

* MP-2344: almost finished the Select

* implement basic vault modal (no logic)

* 🍱  max + displaycur for token input

* Convert to BN for TokenInputs

* env: update wallet-connector

* fix: fixed build errors and relative imports

* fix: updated TokenInput

* tidy: format

* fix: BN instead of new BigNumber

---------

Co-authored-by: Linkie Link <linkielink.dev@gmail.com>
This commit is contained in:
Bob van der Helm 2023-05-02 15:55:32 +08:00 committed by GitHub
parent ac09862f1f
commit cd5ec3ee3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 922 additions and 358 deletions

View File

@ -1,6 +1,6 @@
{
"name": "mars-v2-frontend",
"version": "0.1.0",
"version": "2.0.0",
"private": true,
"scripts": {
"build": "next build",
@ -13,13 +13,13 @@
"dependencies": {
"@cosmjs/cosmwasm-stargate": "^0.30.1",
"@cosmjs/stargate": "^0.30.1",
"@marsprotocol/wallet-connector": "^1.5.3",
"@sentry/nextjs": "^7.47.0",
"@marsprotocol/wallet-connector": "^1.5.4",
"@sentry/nextjs": "^7.48.0",
"@tanstack/react-table": "^8.8.5",
"@tippyjs/react": "^4.2.6",
"bignumber.js": "^9.1.1",
"classnames": "^2.3.2",
"graphql-request": "^5.2.0",
"graphql-request": "^6.0.0",
"moment": "^2.29.4",
"next": "^13.3.0",
"react": "^18.2.0",
@ -30,14 +30,14 @@
"react-toastify": "^9.1.2",
"react-use-clipboard": "^1.0.9",
"recharts": "^2.5.0",
"swr": "^2.1.2",
"swr": "^2.1.3",
"tailwind-scrollbar-hide": "^1.1.7",
"zustand": "^4.3.7"
},
"devDependencies": {
"@svgr/webpack": "^7.0.0",
"@types/node": "^18.15.11",
"@types/react": "18.0.33",
"@types/react": "18.0.35",
"@types/react-dom": "18.0.11",
"autoprefixer": "^10.4.14",
"eslint": "8.38.0",
@ -45,7 +45,7 @@
"eslint-plugin-import": "^2.27.5",
"postcss": "^8.4.21",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.6",
"prettier-plugin-tailwindcss": "^0.2.7",
"tailwindcss": "^3.3.1",
"typescript": "5.0.4"
},

View File

@ -4,6 +4,7 @@ import { headers } from 'next/headers'
import AccountDetails from 'components/Account/AccountDetails'
import Background from 'components/Background'
import FetchPrices from 'components/FetchPrices'
import Footer from 'components/Footer'
import DesktopHeader from 'components/Header/DesktopHeader'
import { Modals } from 'components/Modals'
import Toaster from 'components/Toaster'
@ -23,13 +24,14 @@ export default function RootLayout(props: { children: React.ReactNode }) {
<FetchPrices />
<main
className={classNames(
'relative flex justify-center py-6',
'lg:mt-[65px] lg:h-[calc(100vh-65px)]',
'relative flex justify-center pt-6',
'lg:mt-[65px] lg:h-[calc(100vh-89px)]',
)}
>
<div className='flex max-w-content flex-grow flex-col flex-wrap'>{props.children}</div>
<AccountDetails />
</main>
<Footer />
<Modals />
<Toaster />
</body>

View File

@ -1,6 +1,6 @@
import Card from 'components/Card'
import { Plus, Subtract } from 'components/Icons'
import { Text } from 'components/Text'
import Text from 'components/Text'
interface Props {
items: Item[]

View File

@ -1,7 +1,7 @@
'use client'
import { Gauge } from 'components/Gauge'
import { Heart } from 'components/Icons'
import { Text } from 'components/Text'
import Text from 'components/Text'
import { isNumber } from 'utils/parsers'
import useParams from 'utils/route'

View File

@ -1,8 +1,8 @@
'use client'
import classNames from 'classnames'
import { usePathname, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import AccountStats from 'components/Account/AccountStats'
import { Button } from 'components/Button'
@ -10,13 +10,12 @@ import Card from 'components/Card'
import { ArrowCircledTopRight, ArrowDownLine, ArrowUpLine, TrashBin } from 'components/Icons'
import Radio from 'components/Radio'
import SwitchWithLabel from 'components/SwitchWithLabel'
import { Text } from 'components/Text'
import { ASSETS } from 'constants/assets'
import useParams, { getRoute } from 'utils/route'
import Text from 'components/Text'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { calculateAccountBalance } from 'utils/accounts'
import { hardcodedFee } from 'utils/contants'
import { formatValue } from 'utils/formatters'
import useParams, { getRoute } from 'utils/route'
interface Props {
setShowFundAccount: (showFundAccount: boolean) => void
@ -37,7 +36,7 @@ export default function AccountList(props: Props) {
const deleteAccount = useStore((s) => s.deleteAccount)
const [isLending, setIsLending] = useState(false)
const [isLending, setIsLending] = useToggle()
const accountSelected = !!selectedAccount && !isNaN(Number(selectedAccount))
const selectedAccountDetails = props.accounts.find((account) => account.id === selectedAccount)
const selectedAccountBalance = selectedAccountDetails
@ -52,8 +51,8 @@ export default function AccountList(props: Props) {
}
}
function onChangeLendSwitch(isLending: boolean) {
setIsLending(isLending)
function onChangeLendSwitch() {
setIsLending(!isLending)
/* TODO: handle lending assets */
}

View File

@ -2,7 +2,7 @@
import classNames from 'classnames'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import AccountList from 'components/Account/AccountList'
import CreateAccount from 'components/Account/CreateAccount'
@ -10,15 +10,15 @@ import FundAccount from 'components/Account/FundAccount'
import { Button } from 'components/Button'
import { CircularProgress } from 'components/CircularProgress'
import { Account, Plus, PlusCircled } from 'components/Icons'
import { Overlay } from 'components/Overlay/Overlay'
import { Text } from 'components/Text'
import useParams from 'utils/route'
import Overlay from 'components/Overlay/Overlay'
import Text from 'components/Text'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { hardcodedFee } from 'utils/contants'
import { isNumber } from 'utils/parsers'
import useParams from 'utils/route'
const menuClasses =
'absolute isolate flex w-full flex-wrap overflow-y-scroll scrollbar-hide scroll-smooth'
const menuClasses = 'absolute isolate flex w-full flex-wrap scrollbar-hide'
interface Props {
accounts: Account[]
@ -28,8 +28,8 @@ export default function AccountMenuContent(props: Props) {
const router = useRouter()
const params = useParams()
const createAccount = useStore((s) => s.createAccount)
const [showMenu, setShowMenu] = useState(false)
const [isCreating, setIsCreating] = useState(false)
const [showMenu, setShowMenu] = useToggle()
const [isCreating, setIsCreating] = useToggle()
const selectedAccountId = params.accountId
const hasCreditAccounts = !!props.accounts.length
@ -72,7 +72,10 @@ export default function AccountMenuContent(props: Props) {
: 'Create Account'}
</Button>
<Overlay
className='max-w-screen right-0 mt-2 flex h-[530px] w-[336px] overflow-hidden'
className={classNames(
'max-w-screen right-0 mt-2 flex h-[530px] w-[336px]',
!showFundAccount && 'overflow-hidden',
)}
show={showMenu}
setShow={setShowMenu}
>
@ -96,7 +99,13 @@ export default function AccountMenuContent(props: Props) {
onClick={createAccountHandler}
/>
</div>
<div className={classNames(menuClasses, 'top-[54px] h-[calc(100%-54px)] items-start')}>
<div
className={classNames(
menuClasses,
!showFundAccount && 'overflow-y-scroll scroll-smooth',
'top-[54px] h-[calc(100%-54px)] items-start',
)}
>
{isAccountSelected && isLoadingAccount && (
<div className='flex h-full w-full items-center justify-center p-4'>
<CircularProgress size={40} />
@ -108,7 +117,13 @@ export default function AccountMenuContent(props: Props) {
</div>
</>
) : (
<div className={classNames(menuClasses, 'inset-0 h-full items-end bg-account')}>
<div
className={classNames(
menuClasses,
!showFundAccount && 'overflow-y-scroll scroll-smooth',
'inset-0 h-full items-end bg-account',
)}
>
{showCreateAccount ? (
<CreateAccount createAccount={createAccountHandler} isCreating={isCreating} />
) : showFundAccount ? (

View File

@ -1,7 +1,7 @@
'use client'
import DisplayCurrency from 'components/DisplayCurrency'
import { Heart, Shield } from 'components/Icons'
import { Text } from 'components/Text'
import Text from 'components/Text'
import useStore from 'store'
interface Props {

View File

@ -1,7 +1,7 @@
import Accordion from 'components/Accordion'
import Card from 'components/Card'
import { ArrowChartLineUp, Shield } from 'components/Icons'
import { Text } from 'components/Text'
import Text from 'components/Text'
import useParams from 'utils/route'
export default function AccountSummary() {

View File

@ -2,7 +2,7 @@
import { Button } from 'components/Button'
import { ArrowRight } from 'components/Icons'
import { Text } from 'components/Text'
import Text from 'components/Text'
interface Props {
isCreating: boolean

View File

@ -1,17 +1,19 @@
'use client'
import BigNumber from 'bignumber.js'
import { useCallback, useState } from 'react'
import BigNumber from 'bignumber.js'
import { Button } from 'components/Button'
import { ArrowRight, Cross } from 'components/Icons'
import SwitchWithLabel from 'components/SwitchWithLabel'
import { Text } from 'components/Text'
import Text from 'components/Text'
import TokenInputWithSlider from 'components/TokenInputWithSlider'
import { ASSETS } from 'constants/assets'
import useParams from 'utils/route'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { hardcodedFee } from 'utils/contants'
import { BN } from 'utils/helpers'
import useParams from 'utils/route'
interface Props {
setShowFundAccount: (show: boolean) => void
@ -22,27 +24,34 @@ export default function FundAccount(props: Props) {
const params = useParams()
const deposit = useStore((s) => s.deposit)
const [amount, setAmount] = useState(0)
const [isLending, setIsLending] = useState(false)
const [isFunding, setIsFunding] = useState(false)
const [amount, setAmount] = useState(BN(0))
const [asset, setAsset] = useState<Asset>(ASSETS[0])
const [isLending, setIsLending] = useToggle()
const [isFunding, setIsFunding] = useToggle()
const onChangeAmount = useCallback((amount: number) => {
const onChangeAmount = useCallback((amount: BigNumber) => {
setAmount(amount)
}, [])
const handleLendAssets = useCallback((val: boolean) => {
setIsLending(val)
/* TODO: handle lending assets */
const onChangeAsset = useCallback((asset: Asset) => {
setAsset(asset)
}, [])
const handleLendAssets = useCallback(
(val: boolean) => {
setIsLending(val)
/* TODO: handle lending assets */
},
[setIsLending],
)
async function onDeposit() {
setIsFunding(true)
// TODO: Make this dynamic (token select)
const result = await deposit({
fee: hardcodedFee,
accountId: params.accountId,
coin: {
denom: ASSETS[0].denom,
denom: asset.denom,
amount: amount.toString(),
},
})
@ -73,27 +82,28 @@ export default function FundAccount(props: Props) {
your Osmosis address has no assets.
</Text>
<TokenInputWithSlider
asset={ASSETS[0]}
onChange={onChangeAmount}
onChangeAsset={onChangeAsset}
amount={amount}
max={new BigNumber(1).shiftedBy(ASSETS[0].decimals).toNumber()}
max={BN(1).shiftedBy(ASSETS[0].decimals)}
className='mb-4'
disabled={isFunding}
hasSelect
/>
<div className='mb-4 w-full border-b border-white/10' />
<SwitchWithLabel
name='isLending'
label='Lend assets to earn yield'
value={isLending}
onChange={handleLendAssets}
onChange={() => handleLendAssets(!isLending)}
className='mb-4'
tooltip="Fund your account and lend assets effortlessly! By lending, you'll earn attractive interest (APY) without impacting your LTV. It's a win-win situation - don't miss out on this easy opportunity to grow your holdings!"
disabled={isFunding || amount === 0}
disabled={isFunding || amount.isEqualTo(0)}
/>
<Button
className='w-full'
showProgressIndicator={isFunding}
disabled={amount === 0}
disabled={amount.isEqualTo(0)}
text='Fund Account'
rightIcon={<ArrowRight />}
onClick={onDeposit}

View File

@ -9,10 +9,10 @@ import {
YAxis,
} from 'recharts'
import { formatValue } from 'utils/formatters'
import { FormattedNumber } from 'components/FormattedNumber'
import { Text } from 'components/Text'
import Text from 'components/Text'
import useStore from 'store'
import { formatValue } from 'utils/formatters'
export const RiskChart = ({ data }: RiskChartProps) => {
const enableAnimations = useStore((s) => s.enableAnimations)

View File

@ -17,7 +17,7 @@ import AssetExpanded from 'components/Borrow/AssetExpanded'
import { AssetRow } from 'components/Borrow/AssetRow'
import { ChevronDown, SortAsc, SortDesc, SortNone } from 'components/Icons'
import Loading from 'components/Loading'
import { Text } from 'components/Text'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { getMarketAssets } from 'utils/assets'
import { formatPercent } from 'utils/formatters'

View File

@ -2,7 +2,7 @@ import classNames from 'classnames'
import { useEffect, useState } from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import { Text } from 'components/Text'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import useStore from 'store'
@ -65,7 +65,7 @@ export const BorrowCapacity = ({
className={classNames(
enableAnimations && 'duration-800 transition-[opcity] delay-[1600ms]',
'text-3xs-caps',
limitPercentOfMax ? 'opacity-60' : 'opacity-0',
limitPercentOfMax ? 'opacity-50' : 'opacity-0',
)}
>
<FormattedNumber animate amount={limit} />
@ -137,7 +137,7 @@ export const BorrowCapacity = ({
</div>
</Tooltip>
{!hideValues && (
<div className='mt-2 flex opacity-60 text-3xs-caps'>
<div className='mt-2 flex opacity-50 text-3xs-caps'>
<FormattedNumber animate amount={balance} className='mr-1' />
<span className='mr-1'>of</span>
<FormattedNumber animate amount={max} />

View File

@ -1,5 +1,6 @@
import Image from 'next/image'
import { useState } from 'react'
import BigNumber from 'bignumber.js'
import AccountSummary from 'components/Account/AccountSummary'
import { Button } from 'components/Button'
@ -7,18 +8,19 @@ import Card from 'components/Card'
import Divider from 'components/Divider'
import { ArrowRight } from 'components/Icons'
import { Modal } from 'components/Modal'
import { Text } from 'components/Text'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import TokenInputWithSlider from 'components/TokenInputWithSlider'
import useStore from 'store'
import { hardcodedFee } from 'utils/contants'
import { formatPercent, formatValue } from 'utils/formatters'
import TokenInputWithSlider from 'components/TokenInputWithSlider'
import useParams from 'utils/route'
import { BN } from 'utils/helpers'
export default function BorrowModal() {
const params = useParams()
const [percentage, setPercentage] = useState(0)
const [amount, setAmount] = useState(0)
const [amount, setAmount] = useState(BN(0))
const [selectedAccount, setSelectedAccount] = useState(params.accountId)
const modal = useStore((s) => s.borrowModal)
const borrow = useStore((s) => s.borrow)
@ -33,7 +35,7 @@ export default function BorrowModal() {
function setOpen(isOpen: boolean) {
useStore.setState({ borrowModal: null })
setAmount(0)
setAmount(BN(0))
setPercentage(0)
}
@ -75,7 +77,7 @@ export default function BorrowModal() {
if ((modal.marketData as BorrowAssetActive)?.debt)
debtAmount = Number((modal.marketData as BorrowAssetActive).debt)
const maxAmount = modal.isRepay ? debtAmount : liquidityAmount
const max = BN(modal.isRepay ? debtAmount : liquidityAmount)
return (
<Modal
@ -115,9 +117,12 @@ export default function BorrowModal() {
>
<TokenInputWithSlider
asset={modal.asset}
onChange={setAmount}
onChange={(val) => {
console.log('new value received', val)
setAmount(val)
}}
amount={amount}
max={maxAmount}
max={max}
/>
<Divider />
<Text size='lg'>{modal.isRepay ? 'Repay for' : 'Borrow to'}</Text>

View File

@ -1,7 +1,7 @@
import classNames from 'classnames'
import { ReactElement, ReactNode } from 'react'
import { Text } from 'components/Text'
import Text from 'components/Text'
interface Props {
children: ReactNode

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'
import { Text } from 'components/Text'
import Text from 'components/Text'
import useStore from 'store'
interface Props {

View File

@ -2,7 +2,7 @@ import { Suspense } from 'react'
import Card from 'components/Card'
import Loading from 'components/Loading'
import { Text } from 'components/Text'
import Text from 'components/Text'
async function Content(props: PageProps) {
const address = props.params.address

View File

@ -3,14 +3,12 @@ import BigNumber from 'bignumber.js'
import useStore from 'store'
import { getMarketAssets } from 'utils/assets'
import { FormattedNumber } from './FormattedNumber'
import { BN } from 'utils/helpers'
import { FormattedNumber } from 'components/FormattedNumber'
interface Props {
coin: Coin
className?: string
prefixClassName?: string
valueClassName?: string
isApproximation?: boolean
}
@ -26,7 +24,7 @@ export default function DisplayCurrency(props: Props) {
if (!price || !asset || !displayPrice) return '0'
return new BigNumber(coin.amount)
return BN(coin.amount)
.times(price.amount)
.div(displayPrice.amount)
.integerValue(BigNumber.ROUND_HALF_DOWN)
@ -35,13 +33,14 @@ export default function DisplayCurrency(props: Props) {
return (
<FormattedNumber
className={props.className}
amount={convertToDisplayAmount(props.coin)}
options={{
minDecimals: 0,
maxDecimals: 2,
abbreviated: true,
decimals: displayCurrency.decimals,
prefix: `${props.isApproximation ? '~' : ''}${
prefix: `${props.isApproximation ? '~ ' : ''}${
displayCurrency.prefix ? displayCurrency.prefix : ''
}`,
suffix: displayCurrency.symbol ? ` ${displayCurrency.symbol}` : '',

View File

@ -1,11 +1,10 @@
import { Suspense } from 'react'
import Card from 'components/Card'
import { getVaults } from 'utils/api'
import { Text } from 'components/Text'
import { VaultTable } from 'components/Earn/vault/VaultTable'
import Text from 'components/Text'
import { VAULTS } from 'constants/vaults'
import { VaultTable } from './VaultTable'
import { getVaults } from 'utils/api'
async function Content() {
const vaults = await getVaults()

View File

@ -1,10 +1,8 @@
import { Suspense } from 'react'
import Card from 'components/Card'
import VaultCard from 'components/Earn/vault/VaultCard'
import { getVaults } from 'utils/api'
import { Text } from 'components/Text'
import VaultCard from './VaultCard'
async function Content() {
const vaults = await getVaults()
@ -20,7 +18,13 @@ async function Content() {
contentClassName='grid grid-cols-3'
>
{featuredVaults.map((vault) => (
<VaultCard key={vault.address} vault={vault} />
<VaultCard
key={vault.address}
vault={vault}
title={vault.name}
subtitle='Hot off the presses'
provider={vault.provider}
/>
))}
</Card>
)

View File

@ -1,33 +1,40 @@
'use client'
import Image from 'next/image'
import { Text } from 'components/Text'
import { getAssetByDenom } from 'utils/assets'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { formatPercent, formatValue } from 'utils/formatters'
import { Button } from 'components/Button'
import VaultLogo from 'components/Earn/VaultLogo'
import VaultLogo from 'components/Earn/vault/VaultLogo'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import useStore from 'store'
import { getAssetByDenom } from 'utils/assets'
import { formatPercent, formatValue } from 'utils/formatters'
interface Props {
vault: Vault
title: string
subtitle: string
provider?: string
unbondingPeriod?: number
}
export default function VaultCard(props: Props) {
function openVaultModal() {}
function openVaultModal() {
useStore.setState({ vaultModal: { vault: props.vault } })
}
return (
<div className='border-r-[1px] border-r-white/10 p-4'>
<div className='align-center mb-8 flex justify-between'>
<div>
<Text size='xs' className='mb-2 text-white/60'>
Hot off the presses
{props.subtitle}
</Text>
<span className='flex'>
<Text className='mr-2 font-bold'>{props.vault.name}</Text>
<Text size='sm' className='text-white/60'>
via {props.vault.provider}
</Text>
<Text className='mr-2 font-bold'>{props.title}</Text>
{props.provider && (
<Text size='sm' className='text-white/60'>
via {props.provider}
</Text>
)}
</span>
</div>
<VaultLogo vault={props.vault} />

View File

@ -0,0 +1,53 @@
import { Row } from '@tanstack/react-table'
import VaultCard from 'components/Earn/vault/VaultCard'
import Text from 'components/Text'
import useStore from 'store'
interface Props {
row: Row<Vault>
resetExpanded: (defaultState?: boolean | undefined) => void
}
export default function VaultExpanded(props: Props) {
function enterVaultHandler() {
useStore.setState({ vaultModal: { vault: props.row.original } })
}
return (
<tr
key={props.row.id}
className='cursor-pointer bg-black/20 transition-colors'
onClick={(e) => {
e.preventDefault()
const isExpanded = props.row.getIsExpanded()
props.resetExpanded()
!isExpanded && props.row.toggleExpanded()
}}
>
<td colSpan={5}>
<Text className='border-b border-white/10 px-4 py-5 '>Select bonding period</Text>
<div className='grid grid-cols-3 md:[&>div:nth-child(3)]:border-none'>
<VaultCard
vault={props.row.original}
title='1 day unbonding'
subtitle='$0 deposited'
unbondingPeriod={1}
/>
<VaultCard
vault={props.row.original}
title='7 day unbonding'
subtitle='$0 deposited'
unbondingPeriod={7}
/>
<VaultCard
vault={props.row.original}
title='14 day unbonding'
subtitle='$0 deposited'
unbondingPeriod={14}
/>
</div>
</td>
</tr>
)
}

View File

@ -0,0 +1,114 @@
import BigNumber from 'bignumber.js'
import { useState } from 'react'
import AccountSummary from 'components/Account/AccountSummary'
import { Button } from 'components/Button'
import Card from 'components/Card'
import Divider from 'components/Divider'
import VaultLogo from 'components/Earn/vault/VaultLogo'
import { FormattedNumber } from 'components/FormattedNumber'
import { ArrowRight } from 'components/Icons'
import { Modal } from 'components/Modal'
import Slider from 'components/Slider'
import Switch from 'components/Switch'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import TokenInput from 'components/TokenInput'
import { ASSETS } from 'constants/assets'
import useStore from 'store'
import { getAmount } from 'utils/accounts'
import { formatValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
import useParams from 'utils/route'
export default function VaultModal() {
const modal = useStore((s) => s.vaultModal)
const accounts = useStore((s) => s.accounts)
const params = useParams()
const [amount, setAmount] = useState(BN(0))
const [percentage, setPercentage] = useState(0)
const [isCustomAmount, setIsCustomAmount] = useState(false)
const currentAccount = accounts?.find((account) => account.id === params.accountId)
function handleSwitch() {
setIsCustomAmount(() => !isCustomAmount)
}
function setOpen(isOpen: boolean) {
useStore.setState({ vaultModal: null })
}
function onChangeSlider(value: number) {}
function onChangePrimary(value: BigNumber) {}
function onChangeSecondary(value: BigNumber) {}
if (!modal || !currentAccount) return null
const primaryAsset = ASSETS.find((asset) => asset.denom === modal.vault.denoms.primary)
const secondaryAsset = ASSETS.find((asset) => asset.denom === modal.vault.denoms.secondary)
if (!primaryAsset || !secondaryAsset) return null
const primaryMaxAmount = getAmount(primaryAsset.denom, currentAccount.deposits)
const secondaryMaxAmount = getAmount(secondaryAsset.denom, currentAccount.deposits)
return (
<Modal
open={true}
setOpen={setOpen}
header={
<span className='flex items-center gap-4 px-4'>
<VaultLogo vault={modal.vault} />
<Text>{`${modal.vault.symbols.primary} - ${modal.vault.symbols.secondary}`}</Text>
</span>
}
headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b'
contentClassName='flex flex-col'
>
<div className='flex gap-3 border-b border-b-white/5 px-6 py-4 gradient-header'>
<TitleAndSubCell
title={formatValue(1000000, { abbreviated: true, decimals: 6 })}
sub={'Borrowed'}
/>
<div className='h-100 w-[1px] bg-white/10'></div>
<TitleAndSubCell title={`${1000} (${10000})`} sub={'Liquidity available'} />
</div>
<div className='flex flex-grow items-start gap-6 p-6'>
<Card
className='w-full bg-white/5 p-4'
contentClassName='gap-6 flex flex-col justify-between h-full'
>
<TokenInput
onChange={onChangePrimary}
amount={amount}
max={primaryMaxAmount}
asset={primaryAsset}
/>
<Slider value={percentage} onChange={onChangeSlider} />
<TokenInput
onChange={onChangeSecondary}
amount={amount}
max={secondaryMaxAmount}
asset={secondaryAsset}
/>
<Divider />
<div className='flex justify-between'>
<Text className='text-white/50'>Custom amount</Text>
<Switch checked={isCustomAmount} onChange={handleSwitch} name='customAmount' />
</div>
<div className='flex justify-between'>
<Text className='text-white/50'>{`${modal.vault.symbols.primary}-${modal.vault.symbols.secondary} Position Value`}</Text>
<FormattedNumber amount={0} options={{ prefix: '$' }} />
</div>
<Button
onClick={() => {}}
className='w-full'
text='Continue'
rightIcon={<ArrowRight />}
/>
</Card>
<AccountSummary />
</div>
</Modal>
)
}

View File

@ -11,8 +11,7 @@ export const VaultRow = (props: AssetRowProps) => {
<tr
key={props.row.id}
className={classNames(
'cursor-pointer transition-colors',
props.row.getIsExpanded() ? ' bg-black/20' : 'bg-white/0 hover:bg-white/5',
'bg-white/3 cursor-pointer border-b border-t border-white/5 transition-colors hover:bg-white/5',
)}
onClick={(e) => {
e.preventDefault()

View File

@ -11,15 +11,15 @@ import {
import classNames from 'classnames'
import React from 'react'
import VaultExpanded from 'components/Earn/vault/VaultExpanded'
import VaultLogo from 'components/Earn/vault/VaultLogo'
import { VaultRow } from 'components/Earn/vault/VaultRow'
import { ChevronDown, SortAsc, SortDesc, SortNone } from 'components/Icons'
import { Text } from 'components/Text'
import { getAssetByDenom, getMarketAssets } from 'utils/assets'
import VaultLogo from 'components/Earn/VaultLogo'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { convertPercentage, formatPercent, formatValue } from 'utils/formatters'
import { VAULT_DEPOSIT_BUFFER } from 'constants/vaults'
import { VaultRow } from 'components/Earn/VaultRow'
import { getAssetByDenom } from 'utils/assets'
import { convertPercentage, formatPercent, formatValue } from 'utils/formatters'
type Props = {
data: Vault[]
@ -27,7 +27,6 @@ type Props = {
export const VaultTable = (props: Props) => {
const [sorting, setSorting] = React.useState<SortingState>([])
const marketAssets = getMarketAssets()
const columns = React.useMemo<ColumnDef<Vault>[]>(
() => [
@ -90,7 +89,7 @@ export const VaultTable = (props: Props) => {
),
},
],
[marketAssets, props.data],
[],
)
const table = useReactTable({
@ -157,6 +156,7 @@ export const VaultTable = (props: Props) => {
return (
<React.Fragment key={`${row.id}_subrow`}>
<VaultRow row={row} resetExpanded={table.resetExpanded} />
<VaultExpanded row={row} resetExpanded={table.resetExpanded} />
</React.Fragment>
)
}

15
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,15 @@
import Text from 'components/Text'
import packageInfo from '../../package.json'
export default function Footer() {
return (
<footer className='flex h-6 w-full items-center justify-center'>
<div className='w-full max-w-screen-lg text-right'>
<Text size='xs' className='opacity-50'>
v{packageInfo.version}
</Text>
</div>
</footer>
)
}

View File

@ -1,7 +1,7 @@
import classNames from 'classnames'
import { Text } from 'components/Text'
import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text'
interface ValueData extends FormattedNumberProps {
format?: 'number' | 'string'

View File

@ -1,9 +1,11 @@
'use client'
import BorrowModal from 'components/BorrowModal'
import VaultModal from 'components/Earn/vault/VaultModal'
export const Modals = () => (
<>
<BorrowModal />
<VaultModal />
</>
)

View File

@ -4,47 +4,43 @@ import BigNumber from 'bignumber.js'
import classNames from 'classnames'
import React, { useEffect, useState } from 'react'
import { formatValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
interface Props {
asset: Asset
amount: number
amount: BigNumber
min?: BigNumber
max?: BigNumber
className: string
maxDecimals: number
minValue?: number
max?: number
maxLength?: number
allowNegative?: boolean
style?: {}
disabled?: boolean
onChange: (amount: number) => void
onChange: (amount: BigNumber) => void
onBlur?: () => void
onFocus?: () => void
onRef?: (ref: React.RefObject<HTMLInputElement>) => void
}
function magnify(value: number, asset: Asset) {
return value === 0 ? 0 : new BigNumber(value).shiftedBy(asset.decimals).toNumber()
}
function demagnify(amount: number, asset: Asset) {
return amount === 0 ? 0 : new BigNumber(amount).dividedBy(-1 * asset.decimals).toNumber()
}
export default function NumberInput(props: Props) {
const inputRef = React.useRef<HTMLInputElement>(null)
const cursorRef = React.useRef(0)
const max = props.max ? demagnify(props.max, props.asset) : undefined
// const max = props.max ? demagnify(props.max, props.asset) : undefined
const [inputValue, setInputValue] = useState({
formatted: demagnify(props.amount, props.asset).toString(),
value: demagnify(props.amount, props.asset),
})
const [formattedAmount, setFormattedAmount] = useState(
props.amount.shiftedBy(-1 * props.asset.decimals).toString(),
)
useEffect(() => {
setInputValue({
formatted: demagnify(props.amount, props.asset).toString(),
value: demagnify(props.amount, props.asset),
})
setFormattedAmount(
formatValue(props.amount.toNumber(), {
decimals: props.asset.decimals,
maxDecimals: props.asset.decimals,
thousandSeparator: false,
}),
)
}, [props.amount, props.asset])
useEffect(() => {
@ -58,16 +54,17 @@ export default function NumberInput(props: Props) {
props.onFocus && props.onFocus()
}
const updateValues = (formatted: string, value: number) => {
const updateValues = (formatted: string, amount: BigNumber) => {
const lastChar = formatted.charAt(formatted.length - 1)
if (lastChar === '.') {
cursorRef.current = (inputRef.current?.selectionEnd || 0) + 1
} else {
cursorRef.current = inputRef.current?.selectionEnd || 0
}
setInputValue({ formatted, value })
if (value !== inputValue.value) {
props.onChange(magnify(value, props.asset))
setFormattedAmount(formatted)
console.log(props.amount.toNumber(), amount.toNumber())
if (props.amount.isEqualTo(amount)) {
props.onChange(amount)
}
}
@ -75,7 +72,7 @@ export default function NumberInput(props: Props) {
if (!inputRef.current) return
const cursor = cursorRef.current
const length = inputValue.formatted.length
const length = formattedAmount.length
if (cursor > length) {
inputRef.current.setSelectionRange(length, length)
@ -83,70 +80,72 @@ export default function NumberInput(props: Props) {
}
inputRef.current.setSelectionRange(cursor, cursor)
}, [inputValue, inputRef])
}, [formattedAmount, inputRef])
const onInputChange = (value: string) => {
const numberCount = value.match(/[0-9]/g)?.length || 0
const decimals = value.split('.')[1]?.length || 0
const lastChar = value.charAt(value.length - 1)
const isNumber = !isNaN(Number(value))
const hasMultipleDots = (value.match(/[.,]/g)?.length || 0) > 1
const onInputChange = (formattedAmount: string) => {
const numberCount = formattedAmount.match(/[0-9]/g)?.length || 0
const decimals = formattedAmount.split('.')[1]?.length || 0
const lastChar = formattedAmount.charAt(formattedAmount.length - 1)
const isNumber = !isNaN(Number(formattedAmount))
const hasMultipleDots = (formattedAmount.match(/[.,]/g)?.length || 0) > 1
const isSeparator = lastChar === '.' || lastChar === ','
const isNegative = value.indexOf('-') > -1
const isLowerThanMinimum = props.minValue !== undefined && Number(value) < props.minValue
const isHigherThanMaximum = max !== undefined && Number(value) > max
const isNegative = formattedAmount.indexOf('-') > -1
const isLowerThanMinimum = props.min !== undefined && props.min.isGreaterThan(formattedAmount)
const isHigherThanMaximum = props.max !== undefined && props.max.isLessThan(formattedAmount)
const isTooLong = props.maxLength !== undefined && numberCount > props.maxLength
const exceedsMaxDecimals = props.maxDecimals !== undefined && decimals > props.maxDecimals
if (isNegative && !props.allowNegative) return
if (isSeparator && value.length === 1) {
updateValues('0.', 0)
if (isSeparator && formattedAmount.length === 1) {
updateValues('0.', BN(0))
return
}
if (isSeparator && !hasMultipleDots) {
updateValues(value.replace(',', '.'), inputValue.value)
updateValues(formattedAmount.replace(',', '.'), props.amount)
return
}
if (!isNumber || hasMultipleDots) return
if (exceedsMaxDecimals) {
value = value.substring(0, value.length - 1)
formattedAmount = formattedAmount.substring(0, formattedAmount.length - 1)
}
if (isTooLong) return
if (isLowerThanMinimum) {
updateValues(String(props.minValue), props.minValue!)
if (isLowerThanMinimum && props.min) {
updateValues(String(props.min), props.min)
return
}
if (isHigherThanMaximum) {
updateValues(String(max), max!)
if (isHigherThanMaximum && props.max) {
updateValues(String(props.max), props.max)
return
}
if (lastChar === '0' && Number(value) === Number(inputValue.value)) {
const amount = BN(formattedAmount).shiftedBy(props.asset.decimals)
if (lastChar === '0' && amount.isEqualTo(props.amount)) {
cursorRef.current = (inputRef.current?.selectionEnd || 0) + 1
setInputValue({ ...inputValue, formatted: value })
setFormattedAmount(formattedAmount)
return
}
if (!value) {
updateValues(value, 0)
if (!formattedAmount) {
updateValues(formattedAmount, BN(0))
return
}
updateValues(String(Number(value)), Number(value))
updateValues(formattedAmount, amount)
}
return (
<input
ref={inputRef}
type='text'
value={inputValue.formatted === '0' ? '' : inputValue.formatted}
value={formattedAmount === '0' ? '' : formattedAmount}
onFocus={onInputFocus}
onChange={(e) => onInputChange(e.target.value)}
onBlur={props.onBlur}

View File

@ -5,25 +5,27 @@ interface Props {
children?: ReactNode | string
content?: ReactNode | string
className?: string
hasBackdropIsolation?: boolean
show: boolean
setShow: (show: boolean) => void
}
export const Overlay = ({ children, content, className, show, setShow }: Props) => {
export default function Overlay(props: Props) {
const onClickAway = () => {
setShow(false)
props.setShow(false)
}
return show ? (
return props.show ? (
<>
<div
className={classNames(
'max-w-screen absolute isolate z-50 rounded-sm shadow-overlay backdrop-blur-lg gradient-popover',
'max-w-screen absolute isolate z-50 rounded-sm shadow-overlay backdrop-blur-lg',
props.hasBackdropIsolation ? 'bg-body' : 'gradient-popover',
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-sm before:p-[1px] before:border-glas',
className,
props.className,
)}
>
{children ? children : content}
{props.children ? props.children : props.content}
</div>
<div
className='fixed left-0 top-0 z-40 block h-full w-full hover:cursor-pointer'

View File

@ -3,7 +3,7 @@ import { Suspense } from 'react'
import Card from 'components/Card'
import Loading from 'components/Loading'
import { Text } from 'components/Text'
import Text from 'components/Text'
import { getAccounts } from 'utils/api'
async function Content(props: PageProps) {

View File

@ -0,0 +1,75 @@
import classNames from 'classnames'
import Image from 'next/image'
import DisplayCurrency from 'components/DisplayCurrency'
import Text from 'components/Text'
import { ASSETS } from 'constants/assets'
import { formatValue } from 'utils/formatters'
interface Props extends Option {
isSelected?: boolean
isDisplay?: boolean
onClick?: (value: string) => void
}
export default function Option(props: Props) {
const isCoin = !!props.denom
if (isCoin) {
const currentAsset = ASSETS.find((asset) => asset.denom === props.denom)
const symbol = currentAsset?.symbol ?? ASSETS[0].symbol
const logo = currentAsset?.logo ?? ASSETS[0].logo
const denom = currentAsset?.denom ?? ASSETS[0].denom
const decimals = currentAsset?.decimals ?? ASSETS[0].decimals
const balance = props.amount ?? '0'
if (props.isDisplay) {
return (
<div className={classNames('block bg-white/10 p-3 hover:cursor-pointer')}>{symbol}</div>
)
}
return (
<div
className={classNames(
'grid grid-flow-row grid-cols-5 grid-rows-2 py-3.5 pr-4',
'border-b border-b-white/20 last:border-none',
'hover:cursor-pointer hover:bg-white/20',
props.isSelected && 'bg-white/10',
)}
onClick={() => props?.onClick && props.onClick(denom)}
>
<div className='row-span-2 flex h-full items-center justify-center'>
<Image src={logo} alt={`${symbol} token logo`} width={32} height={32} />
</div>
<Text className='col-span-2 pb-1'>{symbol}</Text>
<Text size='sm' className='col-span-2 pb-1 text-right font-bold'>
{formatValue(balance, { decimals, maxDecimals: 4, minDecimals: 0, rounded: true })}
</Text>
<Text size='sm' className='col-span-2 text-white/50'>
{formatValue(5, { maxDecimals: 2, minDecimals: 0, prefix: 'APY ', suffix: '%' })}
</Text>
<Text size='sm' className='col-span-2 text-right text-white/50'>
<DisplayCurrency coin={{ denom, amount: balance }} />
</Text>
</div>
)
}
const label = props.label
if (props.isDisplay) {
return <div className={classNames('block bg-white/10 p-3 hover:cursor-pointer')}>{label}</div>
}
return (
<div
className={classNames(
'block p-3 hover:cursor-pointer hover:bg-white/20',
props.isSelected && 'bg-white/10',
)}
onClick={() => props?.onClick && props.onClick(props.value)}
>
{label}
</div>
)
}

View File

@ -0,0 +1,82 @@
'use client'
import classNames from 'classnames'
import { useState } from 'react'
import Overlay from 'components/Overlay/Overlay'
import Option from 'components/Select/Option'
import Text from 'components/Text'
import useToggle from 'hooks/useToggle'
interface Props {
options: Option[]
defaultValue?: string
onChange: (value: string) => void
isParent?: boolean
className?: string
title?: string
}
export default function Select(props: Props) {
const [value, setValue] = useState(props.defaultValue)
const selectedOption = value
? props.options.find((option) => option?.value || option?.denom === value)
: null
const [selected, setSelected] = useState<Option>(selectedOption)
const [showDropdown, setShowDropdown] = useToggle()
function handleChange(optionValue: string) {
setValue(optionValue)
setSelected(props.options.find((option) => option?.value || option?.denom === optionValue))
setShowDropdown(false)
props.onChange(optionValue)
}
console.log(selected)
return (
<div
className={classNames(
props.isParent && 'relative',
'flex min-w-fit items-center gap-2',
props.className,
)}
role='select'
onClick={() => setShowDropdown(!showDropdown)}
>
{selectedOption ? (
<Option {...selectedOption} isDisplay />
) : (
<Text className='w-full opacity-50 hover:cursor-pointer'>Select</Text>
)}
<Overlay
show={showDropdown}
className={classNames('left-0 top-[calc(100%+8px)] isolate w-full')}
setShow={setShowDropdown}
hasBackdropIsolation
>
<div className='relative isolate w-full overflow-hidden rounded-sm'>
{props.title && (
<Text size='lg' className='block bg-white/25 p-4 font-bold'>
{props.title}
</Text>
)}
{props.options.map((option: Option, index: number) => (
<Option
key={index}
{...option}
isSelected={
option?.value
? option?.value === selected?.value
: option?.denom === selected?.denom
}
onClick={handleChange}
/>
))}
</div>
</Overlay>
</div>
)
}

View File

@ -1,23 +1,22 @@
'use client'
import { useState } from 'react'
import { Button } from 'components/Button'
import { Gear } from 'components/Icons'
import { Overlay } from 'components/Overlay/Overlay'
import Overlay from 'components/Overlay/Overlay'
import Switch from 'components/Switch'
import { Text } from 'components/Text'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import { ASSETS } from 'constants/assets'
import { DISPLAY_CURRENCY_KEY, ENABLE_ANIMATIONS_KEY } from 'constants/localStore'
import { useAnimations } from 'hooks/useAnimations'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { getDisplayCurrencies } from 'utils/assets'
import { ASSETS } from 'constants/assets'
export default function Settings() {
useAnimations()
const [showMenu, setShowMenu] = useState(false)
const [showMenu, setShowMenu] = useToggle()
const enableAnimations = useStore((s) => s.enableAnimations)
const displayCurrency = useStore((s) => s.displayCurrency)
const displayCurrencies = getDisplayCurrencies()
@ -32,10 +31,10 @@ export default function Settings() {
}
}
function handleReduceMotion(val: boolean) {
useStore.setState({ enableAnimations: !val })
function handleReduceMotion() {
useStore.setState({ enableAnimations: !enableAnimations })
if (typeof window !== 'undefined')
window.localStorage.setItem(ENABLE_ANIMATIONS_KEY, val ? 'false' : 'true')
window.localStorage.setItem(ENABLE_ANIMATIONS_KEY, enableAnimations ? 'false' : 'true')
}
function handleCurrencyChange(e: React.ChangeEvent<HTMLSelectElement>) {

View File

@ -3,6 +3,7 @@ import { ChangeEvent, useRef, useState } from 'react'
import Draggable from 'react-draggable'
import { OverlayMark } from 'components/Icons/index'
import useToggle from 'hooks/useToggle'
type Props = {
value: number
@ -12,11 +13,11 @@ type Props = {
}
export default function Slider(props: Props) {
const [showTooltip, setShowTooltip] = useState(false)
const [showTooltip, setShowTooltip] = useToggle()
const [sliderRect, setSliderRect] = useState({ width: 0, left: 0, right: 0 })
const ref = useRef<HTMLDivElement>(null)
const nodeRef = useRef(null)
const [isDragging, setIsDragging] = useState(false)
const [isDragging, setIsDragging] = useToggle()
function handleSliderRect() {
const leftCap = ref.current?.getBoundingClientRect().left ?? 0

View File

@ -3,7 +3,7 @@ import classNames from 'classnames'
interface Props {
name: string
checked: boolean
onChange: (checked: boolean) => void
onChange: () => void
className?: string
disabled?: boolean
}
@ -23,7 +23,7 @@ export default function Switch(props: Props) {
name={props.name}
className={classNames('peer hidden')}
checked={props.checked}
onChange={(e) => props.onChange(e.target.checked)}
onChange={props.onChange}
/>
<label
htmlFor={props.name}

View File

@ -1,14 +1,14 @@
import classNames from 'classnames'
import Switch from 'components/Switch'
import { Text } from 'components/Text'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
interface Props {
name: string
label: string
value: boolean
onChange: (checked: boolean) => void
onChange: () => void
className?: string
tooltip?: string
disabled?: boolean

View File

@ -13,14 +13,10 @@ interface Props {
const headlines = ['h1', 'h2', 'h3', 'h4']
const headMap = ['6xl', '5xl', '4xl', '3xl']
export const Text = ({
children,
className,
monospace = false,
size = 'base',
tag = 'p',
uppercase = false,
}: Props) => {
export default function Text(props: Props) {
const tag = props.tag ?? 'p'
const size = props.size ?? 'base'
const tagIndex = headlines.indexOf(tag)
const sizeClass = tagIndex > -1 ? headMap[tagIndex] : size
const HtmlElement = tag as keyof JSX.IntrinsicElements
@ -28,12 +24,12 @@ export const Text = ({
return (
<HtmlElement
className={classNames(
className,
uppercase ? `text-${sizeClass}-caps` : `text-${sizeClass}`,
monospace && 'number',
props.className,
props.uppercase ? `text-${sizeClass}-caps` : `text-${sizeClass}`,
props.monospace && 'number',
)}
>
{children}
{props.children}
</HtmlElement>
)
}

View File

@ -1,4 +1,4 @@
import { Text } from 'components/Text'
import Text from 'components/Text'
interface Props {
title: string | React.ReactNode

View File

@ -5,7 +5,7 @@ import { toast as createToast, Slide, ToastContainer } from 'react-toastify'
import { Button } from 'components/Button'
import { CheckCircled, Cross, CrossCircled } from 'components/Icons'
import { Text } from 'components/Text'
import Text from 'components/Text'
import useStore from 'store'
export default function Toaster() {
@ -40,7 +40,7 @@ export default function Toaster() {
</Text>
</div>
<Text size='sm' className='text-bold text-white'>
<Text size='sm' className='font-bold text-white'>
{toast.message}
</Text>
<div className='absolute right-6 top-8 '>

View File

@ -1,19 +1,82 @@
'use client'
import BigNumber from 'bignumber.js'
import classNames from 'classnames'
import Image from 'next/image'
import { useCallback, useEffect, useState } from 'react'
import DisplayCurrency from 'components/DisplayCurrency'
import NumberInput from 'components/NumberInput'
import { Text } from 'components/Text'
import Select from 'components/Select/Select'
import Text from 'components/Text'
import { ASSETS } from 'constants/assets'
import useStore from 'store'
import { magnify } from 'utils/formatters'
import { BN } from 'utils/helpers'
interface Props {
amount: number
max: number
asset: Asset
onChange: (amount: number) => void
amount: BigNumber
onChange: (amount: BigNumber) => void
className?: string
disabled?: boolean
}
export default function TokenInput(props: Props) {
interface SingleProps extends Props {
asset: Asset
max: BigNumber
hasSelect?: boolean
onChangeAsset?: (asset: Asset, max: BigNumber) => void
}
interface SelectProps extends Props {
asset?: Asset
max?: BigNumber
hasSelect: boolean
onChangeAsset: (asset: Asset, max: BigNumber) => void
}
export default function TokenInput(props: SingleProps | SelectProps) {
const balances = useStore((s) => s.balances)
const baseCurrency = useStore((s) => s.baseCurrency)
const [asset, setAsset] = useState<Asset>(props.asset ? props.asset : baseCurrency)
const [coin, setCoin] = useState<Coin>({
denom: props.asset ? props.asset.denom : baseCurrency.denom,
amount: '0',
})
const selectedAssetDenom = props.asset ? props.asset.denom : baseCurrency.denom
const updateAsset = useCallback(
(coinDenom: string) => {
const newAsset = ASSETS.find((asset) => asset.denom === coinDenom) ?? baseCurrency
const newCoin = balances?.find((coin) => coin.denom === coinDenom)
setAsset(newAsset)
setCoin(newCoin ?? { denom: coinDenom, amount: '0' })
},
[balances, baseCurrency],
)
function setDefaultAsset() {
if (!balances || balances?.length === 0) return setAsset(baseCurrency)
if (balances.length === 1)
return setAsset(ASSETS.find((asset) => asset.denom === balances[0].denom) ?? baseCurrency)
return setAsset(ASSETS.find((asset) => asset.denom === selectedAssetDenom) ?? baseCurrency)
}
useEffect(
() => {
setDefaultAsset()
updateAsset(asset.denom)
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
)
useEffect(() => {
props.onChangeAsset && props.onChangeAsset(asset, coin ? BN(coin.amount) : BN(0))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [coin, asset])
return (
<div
className={classNames(
@ -22,28 +85,49 @@ export default function TokenInput(props: Props) {
props.disabled && 'pointer-events-none opacity-50',
)}
>
<div className='box-content flex h-11 w-full rounded-sm border border-white/20 bg-white/5'>
<div className='flex min-w-fit items-center gap-2 border-r border-white/20 bg-white/5 p-3'>
<Image src={props.asset.logo} alt='token' width={20} height={20} />
<Text>{props.asset.symbol}</Text>
</div>
<div className='relative isolate z-40 box-content flex h-11 w-full rounded-sm border border-white/20 bg-white/5'>
{props.hasSelect && balances ? (
<Select
options={balances}
defaultValue={coin.denom}
onChange={(value) => updateAsset(value)}
title='Your Wallet'
className=' border-r border-white/20 bg-white/5'
/>
) : (
<div className='flex min-w-fit items-center gap-2 border-r border-white/20 bg-white/5 p-3'>
<Image src={asset.logo} alt='token' width={20} height={20} />
<Text>{asset.symbol}</Text>
</div>
)}
<NumberInput
disabled={props.disabled}
asset={props.asset}
maxDecimals={props.asset.decimals}
asset={asset}
maxDecimals={asset.decimals}
onChange={props.onChange}
amount={props.amount}
max={props.max}
className='border-none p-3'
/>
</div>
<div className='flex justify-between'>
<Text size='xs' className='text-white/50' monospace>
1 OSMO = $0.9977
</Text>
<Text size='xs' monospace className='text-white/50'>
~ $0.00
</Text>
<div className='flex'>
<div className='flex flex-1'>
<Text size='xs' className='text-white/50' monospace>
{`1 ${asset.symbol} =`}
</Text>
<DisplayCurrency
className='inline pl-0.5 text-xs text-white/50'
coin={{ denom: asset.denom, amount: String(magnify(1, asset)) }}
/>
</div>
<div className='flex'>
<DisplayCurrency
isApproximation
className='inline pl-0.5 text-xs text-white/50'
coin={{ denom: asset.denom, amount: props.amount.toString() }}
/>
</div>
</div>
</div>
)

View File

@ -1,25 +1,41 @@
import BigNumber from 'bignumber.js'
import { useCallback, useState } from 'react'
import Slider from './Slider'
import TokenInput from './TokenInput'
import { BN } from 'utils/helpers'
import Slider from 'components/Slider'
import TokenInput from 'components/TokenInput'
import { ASSETS } from 'constants/assets'
interface Props {
amount: number
max: number
asset: Asset
onChange: (amount: number) => void
amount: BigNumber
onChange: (amount: BigNumber) => void
className?: string
disabled?: boolean
}
export default function TokenInputWithSlider(props: Props) {
interface SingleProps extends Props {
max: BigNumber
asset: Asset
hasSelect?: boolean
onChangeAsset?: (asset: Asset) => void
}
interface SelectProps extends Props {
max?: BigNumber
asset?: Asset
onChangeAsset: (asset: Asset) => void
hasSelect: boolean
}
export default function TokenInputWithSlider(props: SingleProps | SelectProps) {
const [amount, setAmount] = useState(props.amount)
const [percentage, setPercentage] = useState(0)
const [asset, setAsset] = useState<Asset>(props.asset ? props.asset : ASSETS[0])
const [max, setMax] = useState<BigNumber>(props.max ? props.max : BN(0))
const onSliderChange = useCallback(
(percentage: number, liquidityAmount: number) => {
const newAmount = new BigNumber(percentage).div(100).times(liquidityAmount).toNumber()
(percentage: number, liquidityAmount: BigNumber) => {
const newAmount = BN(percentage).div(100).times(liquidityAmount)
setPercentage(percentage)
setAmount(newAmount)
props.onChange(newAmount)
@ -28,27 +44,40 @@ export default function TokenInputWithSlider(props: Props) {
)
const onInputChange = useCallback(
(newAmount: number, liquidityAmount: number) => {
(newAmount: BigNumber, liquidityAmount: BigNumber) => {
setAmount(newAmount)
setPercentage(new BigNumber(newAmount).div(liquidityAmount).times(100).toNumber())
setPercentage(BN(newAmount).div(liquidityAmount).times(100).toNumber())
props.onChange(newAmount)
},
[props],
)
const onAssetChange = useCallback(
(newAsset: Asset, liquidtyAmount: BigNumber) => {
props.onChangeAsset && props.onChangeAsset(newAsset)
setAsset(newAsset)
setMax(liquidtyAmount)
setPercentage(0)
setAmount(BN(0))
},
[props],
)
return (
<div className={props.className}>
<TokenInput
asset={props.asset}
onChange={(amount) => onInputChange(amount, props.max)}
asset={asset}
onChange={(amount) => onInputChange(amount, max)}
onChangeAsset={(asset: Asset, max: BigNumber) => onAssetChange(asset, max)}
amount={amount}
max={props.max}
max={max}
className='mb-4'
disabled={props.disabled}
hasSelect
/>
<Slider
value={percentage}
onChange={(value) => onSliderChange(value, props.max)}
onChange={(value) => onSliderChange(value, max)}
disabled={props.disabled}
/>
</div>

View File

@ -2,7 +2,7 @@ import { Suspense } from 'react'
import Card from 'components/Card'
import Loading from 'components/Loading'
import { Text } from 'components/Text'
import Text from 'components/Text'
async function Content(props: PageProps) {
const address = props.params.address

View File

@ -2,7 +2,7 @@ import { Suspense } from 'react'
import Card from 'components/Card'
import Loading from 'components/Loading'
import { Text } from 'components/Text'
import Text from 'components/Text'
async function Content(props: PageProps) {
const address = props.params.address

View File

@ -2,7 +2,7 @@ import { Suspense } from 'react'
import Card from 'components/Card'
import Loading from 'components/Loading'
import { Text } from 'components/Text'
import Text from 'components/Text'
async function Content(props: PageProps) {
return <Text size='sm'>Chart view</Text>

View File

@ -16,17 +16,19 @@ import { Button } from 'components/Button'
import { CircularProgress } from 'components/CircularProgress'
import { FormattedNumber } from 'components/FormattedNumber'
import { Check, Copy, ExternalLink, Osmo } from 'components/Icons'
import { Overlay } from 'components/Overlay/Overlay'
import { Text } from 'components/Text'
import Overlay from 'components/Overlay/Overlay'
import Text from 'components/Text'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { Endpoints, getEndpoint, getWalletBalancesSWR } from 'utils/api'
import { getBaseAsset } from 'utils/assets'
import { getBaseAsset, getMarketAssets } from 'utils/assets'
import { formatValue, truncate } from 'utils/formatters'
export default function ConnectedButton() {
// ---------------
// EXTERNAL HOOKS
// ---------------
const marketAssets = getMarketAssets()
const { disconnect } = useWallet()
const { disconnect: terminate } = useWalletManager()
const address = useStore((s) => s.client?.recentWallet.account?.address)
@ -40,7 +42,7 @@ export default function ConnectedButton() {
// ---------------
// LOCAL STATE
// ---------------
const [showDetails, setShowDetails] = useState(false)
const [showDetails, setShowDetails] = useToggle()
const [walletAmount, setWalletAmount] = useState(0)
const [isCopied, setCopied] = useClipboard(address || '', {
successDuration: 1000 * 5,
@ -59,7 +61,7 @@ export default function ConnectedButton() {
const disconnectWallet = () => {
disconnect()
terminate()
useStore.setState({ client: undefined })
useStore.setState({ client: undefined, balances: null })
}
useEffect(() => {
@ -69,7 +71,11 @@ export default function ConnectedButton() {
.div(10 ** baseAsset.decimals)
.toNumber(),
)
}, [data, baseAsset.denom, baseAsset.decimals])
const assetDenoms = marketAssets.map((asset) => asset.denom)
const balances = data.filter((coin) => assetDenoms.includes(coin.denom))
useStore.setState({ balances })
}, [data, baseAsset.denom, baseAsset.decimals, marketAssets])
return (
<div className={'relative'}>
@ -102,7 +108,7 @@ export default function ConnectedButton() {
{isLoading ? (
<CircularProgress size={12} />
) : (
`${formatValue(walletAmount, { suffix: baseAsset.symbol })}`
`${formatValue(walletAmount, { suffix: ` ${baseAsset.symbol}` })}`
)}
</div>
</Button>

View File

@ -1,13 +1,13 @@
import Card from 'components/Card'
import Tab from 'components/Earn/Tab'
import AvailableVaults from 'components/Earn/vault/AvailableVaults'
import FeaturedVaults from 'components/Earn/vault/FeaturedVaults'
export default function Farmpage({ params }: { params: PageParams }) {
return (
<>
<Tab params={params} isFarm />
<Card title='Featured vaults'>
<></>
</Card>
<FeaturedVaults />
<AvailableVaults />
</>
)
}

14
src/hooks/useToggle.tsx Normal file
View File

@ -0,0 +1,14 @@
import { useState } from 'react'
export default function useToggle(
defaultValue?: boolean,
): [boolean, (isToggled?: boolean) => void] {
const [toggle, setToggle] = useState<boolean>(defaultValue ?? false)
function handleToggle(isToggled?: boolean) {
if (isToggled !== undefined) return setToggle(isToggled)
return setToggle(!toggle)
}
return [toggle, handleToggle]
}

View File

@ -0,0 +1,21 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { ENV, ENV_MISSING_MESSAGE, VERCEL_BYPASS } from 'constants/env'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!ENV.URL_RPC || !ENV.ADDRESS_CREDIT_MANAGER || !ENV.URL_API) {
return res.status(404).json(ENV_MISSING_MESSAGE)
}
const accountId = req.query.id
const account: Account = await (
await fetch(`${ENV.URL_API}/accounts/${accountId}${VERCEL_BYPASS}`)
).json()
if (account) {
return res.status(200).json(account.vaults)
}
return res.status(404)
}

View File

@ -1,7 +1,7 @@
import BigNumber from 'bignumber.js'
import { NextApiRequest, NextApiResponse } from 'next'
import { ENV, ENV_MISSING_MESSAGE, VERCEL_BYPASS } from 'constants/env'
import { BN } from 'utils/helpers'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!ENV.URL_API) {
@ -28,7 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
borrowRate: market.borrowRate ?? 0,
liquidity: {
amount: amount,
value: new BigNumber(amount).times(price).toString(),
value: BN(amount).times(price).toString(),
},
}
})

View File

@ -1,4 +1,3 @@
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { WalletClient, WalletConnectionStatus } from '@marsprotocol/wallet-connector'
import { GetState, SetState } from 'zustand'
@ -7,6 +6,7 @@ export interface CommonSlice {
address?: string
enableAnimations: boolean
isOpen: boolean
balances: Coin[] | null
selectedAccount: string | null
client?: WalletClient
status: WalletConnectionStatus
@ -15,6 +15,7 @@ export interface CommonSlice {
export function createCommonSlice(set: SetState<CommonSlice>, get: GetState<CommonSlice>) {
return {
accounts: null,
balances: null,
creditAccounts: null,
enableAnimations: true,
isOpen: true,

View File

@ -10,6 +10,9 @@ export interface ModalSlice {
deleteAccountModal: boolean
fundAccountModal: boolean
withdrawModal: boolean
vaultModal: {
vault: Vault
} | null
}
export function createModalSlice(set: SetState<ModalSlice>, get: GetState<ModalSlice>) {
@ -19,5 +22,6 @@ export function createModalSlice(set: SetState<ModalSlice>, get: GetState<ModalS
deleteAccountModal: false,
fundAccountModal: false,
withdrawModal: false,
vaultModal: null,
}
}

View File

@ -3,4 +3,5 @@ interface Account {
deposits: Coin[]
debts: Coin[]
lends: Coin[]
vaults: import('types/generated/mars-mock-credit-manager/MarsMockCreditManager.types').ArrayOfVaultInfoResponse
}

1
src/types/interfaces/option.d.ts vendored Normal file
View File

@ -0,0 +1 @@
type Option = import('cosmjs/stargate').Coin | { value: string; label: string }

View File

@ -3,6 +3,7 @@ interface AccountResponse {
deposits: Coin[]
debts: Coin[]
lends: Coin[]
vaults: import('types/generated/mars-mock-credit-manager/MarsMockCreditManager.types').ArrayOfVaultInfoResponse
}
interface MarketResponse {

View File

@ -33,3 +33,16 @@ interface VaultConfig extends VaultMetaData {
interface Vault extends VaultConfig {
apy: number | null
}
interface ActiveVault extends Vault {
status: 'active' | 'unlocking' | 'unlocked'
amounts: {
primary: number
secondary: number
}
values: {
primary: number
secondary: number
}
unlocksAt?: number
}

View File

@ -1,17 +1,23 @@
import BigNumber from 'bignumber.js'
import { BN } from './helpers'
export const calculateAccountBalance = (account: Account, prices: Coin[]) => {
const totalDepositValue = account.deposits.reduce((acc, deposit) => {
const price = prices.find((price) => price.denom === deposit.denom)?.amount ?? 0
const depositValue = new BigNumber(deposit.amount).multipliedBy(price)
const depositValue = BN(deposit.amount).multipliedBy(price)
return acc.plus(depositValue)
}, new BigNumber(0))
}, BN(0))
const totalDebtValue = account.debts.reduce((acc, debt) => {
const price = prices.find((price) => price.denom === debt.denom)?.amount ?? 0
const debtValue = new BigNumber(debt.amount).multipliedBy(price)
const debtValue = BN(debt.amount).multipliedBy(price)
return acc.plus(debtValue)
}, new BigNumber(0))
}, BN(0))
return totalDepositValue.minus(totalDebtValue).toNumber()
}
export function getAmount(denom: string, coins: Coin[]) {
return BN(coins.find((asset) => asset.denom === denom)?.amount ?? 0)
}

View File

@ -1,6 +1,5 @@
import BigNumber from 'bignumber.js'
import { getMarketAssets } from './assets'
import { BN } from './helpers'
export function truncate(text = '', [h, t]: [number, number] = [6, 6]): string {
const head = text.slice(0, h)
@ -33,7 +32,7 @@ export const formatValue = (amount: number | string, options?: FormatOptions): s
numberOfZeroDecimals = decimals.length
}
}
let convertedAmount: number | string = new BigNumber(amount)
let convertedAmount: number | string = BN(amount)
.dividedBy(10 ** (options?.decimals ?? 0))
.toNumber()
@ -143,3 +142,15 @@ export const convertPercentage = (percent: number) => {
if (percent !== 0 && percent < 0.01) percentage = 0.01
return Number(formatValue(percentage, { minDecimals: 0, maxDecimals: 0 }))
}
export function magnify(value: number, asset: Asset) {
return value === 0 ? 0 : BN(value).shiftedBy(asset.decimals).toNumber()
}
export function demagnify(amount: number, asset: Asset) {
return amount === 0
? 0
: BN(amount)
.shiftedBy(-1 * asset.decimals)
.toNumber()
}

5
src/utils/helpers.ts Normal file
View File

@ -0,0 +1,5 @@
import BigNumber from 'bignumber.js'
export function BN(n: BigNumber.Value) {
return new BigNumber(n)
}

View File

@ -8,6 +8,7 @@ export function resolvePositionResponse(response: AccountResponse): Account {
deposits: response.deposits,
debts: response.debts,
lends: response.lends,
vaults: response.vaults,
}
}

201
yarn.lock
View File

@ -1901,6 +1901,11 @@
resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.2.tgz"
integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA==
"@graphql-typed-document-node/core@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
"@humanwhocodes/config-array@^0.11.8":
version "0.11.8"
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz"
@ -2264,10 +2269,10 @@
big-integer "^1.6.48"
utility-types "^3.10.0"
"@marsprotocol/wallet-connector@^1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@marsprotocol/wallet-connector/-/wallet-connector-1.5.3.tgz#b77c814f7e5b824b9b8babf4f5a0a6879e90ffba"
integrity sha512-XBAy+Kn0kVoE+l/jif7Q9Q/7uC3rTVuFaDMmL7ks2p85ImCO0HQFpvZKv901jguDM230vKI+Hs942CszaS21WA==
"@marsprotocol/wallet-connector@^1.5.4":
version "1.5.4"
resolved "https://registry.yarnpkg.com/@marsprotocol/wallet-connector/-/wallet-connector-1.5.4.tgz#81d0e213e9e9c4fff78781cd49fd627b75076fe6"
integrity sha512-RkMVyFkSEDrDcb8SnBnOA3wVWT/zaNdzpgbLpO+Zn8vsYGea8uugafSKCeodKNi83/Q2rBEbTUqMJeyLYI9OMQ==
dependencies:
"@cosmjs/cosmwasm-stargate" "^0.30.1"
"@cosmjs/encoding" "^0.30.1"
@ -2597,26 +2602,26 @@
"@noble/hashes" "~1.2.0"
"@scure/base" "~1.1.0"
"@sentry-internal/tracing@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.47.0.tgz#45e92eb4c8d049d93bd4fab961eaa38a4fb680f3"
integrity sha512-udpHnCzF8DQsWf0gQwd0XFGp6Y8MOiwnl8vGt2ohqZGS3m1+IxoRLXsSkD8qmvN6KKDnwbaAvYnK0z0L+AW95g==
"@sentry-internal/tracing@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.48.0.tgz#d0c1eac1c046fda5c79d16dc1c918fee3bae3e9d"
integrity sha512-MFAPDTrvCtfSm0/Zbmx7HA0Q5uCfRadOUpN8Y8rP1ndz+329h2kA3mZRCuC+3/aXL11zs2CHUhcAkGjwH2vogg==
dependencies:
"@sentry/core" "7.47.0"
"@sentry/types" "7.47.0"
"@sentry/utils" "7.47.0"
"@sentry/core" "7.48.0"
"@sentry/types" "7.48.0"
"@sentry/utils" "7.48.0"
tslib "^1.9.3"
"@sentry/browser@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.47.0.tgz#c0d10f348d1fb9336c3ef8fa2f6638f26d4c17a8"
integrity sha512-L0t07kS/G1UGVZ9fpD6HLuaX8vVBqAGWgu+1uweXthYozu/N7ZAsakjU/Ozu6FSXj1mO3NOJZhOn/goIZLSj5A==
"@sentry/browser@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.48.0.tgz#03f39bec6949ff48e343c5862c5d54dfd4a2f9ff"
integrity sha512-tdx/2nhuiykncmXFlV4Dpp+Hxgt/v31LiyXE79IcM560wc+QmWKtzoW9azBWQ0xt5KOO3ERMib9qPE4/ql1/EQ==
dependencies:
"@sentry-internal/tracing" "7.47.0"
"@sentry/core" "7.47.0"
"@sentry/replay" "7.47.0"
"@sentry/types" "7.47.0"
"@sentry/utils" "7.47.0"
"@sentry-internal/tracing" "7.48.0"
"@sentry/core" "7.48.0"
"@sentry/replay" "7.48.0"
"@sentry/types" "7.48.0"
"@sentry/utils" "7.48.0"
tslib "^1.9.3"
"@sentry/cli@^1.74.6":
@ -2631,88 +2636,88 @@
proxy-from-env "^1.1.0"
which "^2.0.2"
"@sentry/core@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.47.0.tgz#6a723d96f64009a9c1b9bc44e259956b7eca0a3f"
integrity sha512-EFhZhKdMu7wKmWYZwbgTi8FNZ7Fq+HdlXiZWNz51Bqe3pHmfAkdHtAEs0Buo0v623MKA0CA4EjXIazGUM34XTg==
"@sentry/core@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.48.0.tgz#1a5ec347ab7212d73a99583c2e64989e34e3263a"
integrity sha512-8FYuJTMpyuxRZvlen3gQ3rpOtVInSDmSyXqWEhCLuG/w34AtWoTiW7G516rsAAh6Hy1TP91GooMWbonP3XQNTQ==
dependencies:
"@sentry/types" "7.47.0"
"@sentry/utils" "7.47.0"
"@sentry/types" "7.48.0"
"@sentry/utils" "7.48.0"
tslib "^1.9.3"
"@sentry/integrations@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.47.0.tgz#b952cc910e92e9235f42151f7260471b55b10cdf"
integrity sha512-PUSeBYI3fCOswn+K+PLjtl2epr8/ceqebWqVcxHclczSY3EOZE+osznDFgZmeVgrHavsgfE4oFVqJeFvDJwCog==
"@sentry/integrations@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.48.0.tgz#1f1c3fd0735b3c40944a42fb45e3e0ca40f77d6d"
integrity sha512-yzbJopVu1UHFXRDv236o5hSEUtqeP45T9uSVbAhKnH5meKWunK7MKvhFvQjhcfvlUVibYrewoVztQP2hrpxgfw==
dependencies:
"@sentry/types" "7.47.0"
"@sentry/utils" "7.47.0"
"@sentry/types" "7.48.0"
"@sentry/utils" "7.48.0"
localforage "^1.8.1"
tslib "^1.9.3"
"@sentry/nextjs@^7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.47.0.tgz#72c0a3ecea87940db3eadeb94f6b47eb8f3ead64"
integrity sha512-KcvN0l5N819LdX7iFUrZjYTX5ITm8lXmiOSyhiLTZBm68ZZbmX2TMrMMlGCLuc0qBZQolu11u6gVQSfTaZPQ9Q==
"@sentry/nextjs@^7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.48.0.tgz#e1c606eb31a907cdc00f96baebffdba3560405b5"
integrity sha512-SLWkd1ZB27uK21QkUiIBEUgvhaMFMx8V5MO2+IlGluJKUdd06IgYAOsS0kjwQc34Ow6D0qowy8iScmtHebgQew==
dependencies:
"@rollup/plugin-commonjs" "24.0.0"
"@sentry/core" "7.47.0"
"@sentry/integrations" "7.47.0"
"@sentry/node" "7.47.0"
"@sentry/react" "7.47.0"
"@sentry/types" "7.47.0"
"@sentry/utils" "7.47.0"
"@sentry/core" "7.48.0"
"@sentry/integrations" "7.48.0"
"@sentry/node" "7.48.0"
"@sentry/react" "7.48.0"
"@sentry/types" "7.48.0"
"@sentry/utils" "7.48.0"
"@sentry/webpack-plugin" "1.20.0"
chalk "3.0.0"
rollup "2.78.0"
stacktrace-parser "^0.1.10"
tslib "^1.9.3"
"@sentry/node@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.47.0.tgz#2c1a8c4777eaf20232fc16d3aa2f26f3fd04bfd1"
integrity sha512-LTg2r5EV9yh4GLYDF+ViSltR9LLj/pcvk8YhANJcMO3Fp//xh8njcdU0FC2yNthUREawYDzAsVzLyCYJfV0H1A==
"@sentry/node@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.48.0.tgz#b2f15502b77796bf7bcaa29f2e9ce1420f7c49d1"
integrity sha512-DJyyZaVhv/pUzJPof7es6zYDHeWbNqE0T3tQfLCkShdyfR+Ew8In8W/x2s7S8vq0cfRq0rqv1E6B2/HpVdYO7g==
dependencies:
"@sentry-internal/tracing" "7.47.0"
"@sentry/core" "7.47.0"
"@sentry/types" "7.47.0"
"@sentry/utils" "7.47.0"
"@sentry-internal/tracing" "7.48.0"
"@sentry/core" "7.48.0"
"@sentry/types" "7.48.0"
"@sentry/utils" "7.48.0"
cookie "^0.4.1"
https-proxy-agent "^5.0.0"
lru_map "^0.3.3"
tslib "^1.9.3"
"@sentry/react@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.47.0.tgz#9b0b937465a4e7fc6c3bde90ef9d0be2ef708b78"
integrity sha512-Qy6OnlE8FivKOLo0YE7tkr+G5fLmEOkpPxj179wbY/N8kp/ALkqbVdcOrZW7AL6HCc0lphhj+0SB+tpwoPEsiQ==
"@sentry/react@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.48.0.tgz#3c160d7dcccc7581b57c430fccfe482c912c3f0d"
integrity sha512-E2HF0njufOI/BWktXfIiPNIh0dh7la9uQmDlYiFAK8MnlW4OOjw4rRJV2qkxKQCYdO9WB+T460DVw102Z/MyUA==
dependencies:
"@sentry/browser" "7.47.0"
"@sentry/types" "7.47.0"
"@sentry/utils" "7.47.0"
"@sentry/browser" "7.48.0"
"@sentry/types" "7.48.0"
"@sentry/utils" "7.48.0"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/replay@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.47.0.tgz#d2fc8fd3be2360950497426035d1ba0bd8a97b8f"
integrity sha512-BFpVZVmwlezZ83y0L43TCTJY142Fxh+z+qZSwTag5HlhmIpBKw/WKg06ajOhrYJbCBkhHmeOvyKkxX0jnc39ZA==
"@sentry/replay@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.48.0.tgz#ca8f1543bad4717dcd65739bf1256a1933bba757"
integrity sha512-8fRHMGJ0NJeIZi6UucxUTvfDPaBa7+jU1kCTLjCcuH3X/UVz5PtGLMtFSO5U8HP+mUDlPs97MP1uoDvMa4S2Ng==
dependencies:
"@sentry/core" "7.47.0"
"@sentry/types" "7.47.0"
"@sentry/utils" "7.47.0"
"@sentry/core" "7.48.0"
"@sentry/types" "7.48.0"
"@sentry/utils" "7.48.0"
"@sentry/types@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.47.0.tgz#fd07dbec11a26ae861532a9abe75bd31663ca09b"
integrity sha512-GxXocplN0j1+uczovHrfkykl9wvkamDtWxlPUQgyGlbLGZn+UH1Y79D4D58COaFWGEZdSNKr62gZAjfEYu9nQA==
"@sentry/types@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.48.0.tgz#57f3c9cf331a5621e82dda04eefcf8c19ee42bc9"
integrity sha512-kkAszZwQ5/v4n7Yyw/DPNRWx7h724mVNRGZIJa9ggUMvTgMe7UKCZZ5wfQmYiKVlGbwd9pxXAcP8Oq15EbByFQ==
"@sentry/utils@7.47.0":
version "7.47.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.47.0.tgz#e62fdede15e45387b40c9fa135feba48f0960826"
integrity sha512-A89SaOLp6XeZfByeYo2C8Ecye/YAtk/gENuyOUhQEdMulI6mZdjqtHAp7pTMVgkBc/YNARVuoa+kR/IdRrTPkQ==
"@sentry/utils@7.48.0":
version "7.48.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.48.0.tgz#2866975ea8899aba35b083dd0558cbbe29ee8de1"
integrity sha512-d977sghkFVMfld0LrEyyY2gYrfayLPdDEpUDT+hg5y79r7zZDCFyHtdB86699E5K89MwDZahW7Erk+a1nk4x5w==
dependencies:
"@sentry/types" "7.47.0"
"@sentry/types" "7.48.0"
tslib "^1.9.3"
"@sentry/webpack-plugin@1.20.0":
@ -3032,10 +3037,10 @@
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@18.0.33":
version "18.0.33"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.33.tgz#a1575160cb4376787c2f5fe0312302f824baa61e"
integrity sha512-sHxzVxeanvQyQ1lr8NSHaj0kDzcNiGpILEVt69g9S31/7PfMvNCKLKcsHw4lYKjs3cGNJjXSP4mYzX43QlnjNA==
"@types/react@18.0.35":
version "18.0.35"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.35.tgz#192061cb1044fe01f2d3a94272cd35dd50502741"
integrity sha512-6Laome31HpetaIUGFWl1VQ3mdSImwxtFZ39rh059a1MNnKGqBpC88J6NJ8n/Is3Qx7CefDGLgf/KhN/sYCf7ag==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@ -4985,11 +4990,6 @@ exenv@^1.2.0:
resolved "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz"
integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==
extract-files@^9.0.0:
version "9.0.0"
resolved "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz"
integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
@ -5093,15 +5093,6 @@ for-each@^0.3.3:
dependencies:
is-callable "^1.1.3"
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz"
integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz"
@ -5318,15 +5309,13 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
graphql-request@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/graphql-request/-/graphql-request-5.2.0.tgz"
integrity sha512-pLhKIvnMyBERL0dtFI3medKqWOz/RhHdcgbZ+hMMIb32mEPa5MJSzS4AuXxfI4sRAu6JVVk5tvXuGfCWl9JYWQ==
graphql-request@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.0.0.tgz#9c8b6a0c341f289e049936d03cc9205300faae1c"
integrity sha512-2BmHTuglonjZvmNVw6ZzCfFlW/qkIPds0f+Qdi/Lvjsl3whJg2uvHmSvHnLWhUTEw6zcxPYAHiZoPvSVKOZ7Jw==
dependencies:
"@graphql-typed-document-node/core" "^3.1.1"
"@graphql-typed-document-node/core" "^3.2.0"
cross-fetch "^3.1.5"
extract-files "^9.0.0"
form-data "^3.0.0"
graphql-tag@^2.12.6:
version "2.12.6"
@ -6546,10 +6535,10 @@ prelude-ls@^1.2.1:
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-plugin-tailwindcss@^0.2.6:
version "0.2.6"
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.6.tgz#d57e3d960f1a968bf16d2a5846e94f95d6b8add2"
integrity sha512-F+7XCl9RLF/LPrGdUMHWpsT6TM31JraonAUyE6eBmpqymFvDwyl0ETHsKFHP1NG+sEfv8bmKqnTxEbWQbHPlBA==
prettier-plugin-tailwindcss@^0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.7.tgz#2314d728cce9c9699ced41a01253eb48b4218da5"
integrity sha512-jQopIOgjLpX+y8HeD56XZw7onupRTC0cw7eKKUimI7vhjkPF5/1ltW5LyqaPtSyc8HvEpvNZsvvsGFa2qpa59w==
prettier@^2.8.7:
version "2.8.7"
@ -7351,10 +7340,10 @@ svgo@^3.0.2:
csso "^5.0.5"
picocolors "^1.0.0"
swr@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.1.2.tgz#15841cf5bbb8b20f24e2408193f616a41b6734a0"
integrity sha512-ocfaD2rnYZKqTDplCEX2bH5Z1++n2JSej9oYi7hVfXXWYm+0RP+H6fVrogWB0mtMclv1guk9kEnAzNLygOy9Hw==
swr@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.1.3.tgz#ae3608d4dc19f75121e0b51d1982735fbf02f52e"
integrity sha512-g3ApxIM4Fjbd6vvEAlW60hJlKcYxHb+wtehogTygrh6Jsw7wNagv9m4Oj5Gq6zvvZw0tcyhVGL9L0oISvl3sUw==
dependencies:
use-sync-external-store "^1.2.0"