Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: market badges #654

Merged
merged 27 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5868805
refactor: rename PoolTitleCell
DanielSchiavini Jan 29, 2025
fac74ef
feat: TokenPair component with chain icon
DanielSchiavini Jan 29, 2025
f54e51f
Merge branch 'feat/mui-chip' into feat/pool-badges
DanielSchiavini Jan 31, 2025
ba2b325
feat: market badges (with fake data)
DanielSchiavini Jan 31, 2025
0f14636
feat: only show favorite button on hover
DanielSchiavini Jan 31, 2025
315e3e4
feat: market link
DanielSchiavini Jan 31, 2025
9fdcb96
fix: connect wallet in llama markets
DanielSchiavini Jan 31, 2025
6240e79
refactor: delete MarketLink component
DanielSchiavini Jan 31, 2025
61fe630
Merge branch 'feat/mui-chip' into feat/pool-badges
DanielSchiavini Feb 3, 2025
3009f12
refactor: use prices API
DanielSchiavini Feb 3, 2025
ac9ca38
feat: MarketFilterChips
DanielSchiavini Feb 3, 2025
3dcefa3
Merge branch 'main' of github.com:curvefi/curve-frontend into feat/po…
DanielSchiavini Feb 4, 2025
4cd3fb6
Merge branch 'main' of github.com:curvefi/curve-frontend into feat/po…
DanielSchiavini Feb 4, 2025
3cb1917
feat: implement campaigns and leverage
DanielSchiavini Feb 4, 2025
a202cf3
feat: implement market filter chips
DanielSchiavini Feb 4, 2025
911b80a
feat: reset filters button
DanielSchiavini Feb 4, 2025
49c882f
fix: remove first market type
DanielSchiavini Feb 4, 2025
3b48fcf
fix: Chip on hover
DanielSchiavini Feb 4, 2025
895cc0c
feat: animate delete button
DanielSchiavini Feb 4, 2025
c39ad4e
fix: row borders, copy color
DanielSchiavini Feb 4, 2025
01c128c
Merge branch 'main' of github.com:curvefi/curve-frontend into feat/po…
DanielSchiavini Feb 4, 2025
7d1c835
chore: avoid fetch_on_chain, update query keys
DanielSchiavini Feb 4, 2025
1517d85
Merge branch 'main' of github.com:curvefi/curve-frontend into feat/po…
DanielSchiavini Feb 11, 2025
15b76d6
Merge branch 'main' of github.com:curvefi/curve-frontend into feat/po…
DanielSchiavini Feb 12, 2025
3f3550f
fix: review comments
DanielSchiavini Feb 12, 2025
b820ec2
test: market badges
DanielSchiavini Feb 12, 2025
4537c5a
fix: self-review
DanielSchiavini Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/main/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ const { Spacing } = SizesAndSpaces
* This is used in the lending markets filters to display collateral and debt tokens.
*/
const Token = ({ symbol, data, field }: { symbol: string; data: LlamaMarket[]; field: 'collateral' | 'borrowed' }) => {
const { blockchainId, address } = useMemo(
const { chain, address } = useMemo(
() => data.find((d) => d.assets[field].symbol === symbol)!.assets[field],
[data, field, symbol],
)
return (
<>
<TokenIcon imageBaseUrl={getImageBaseUrl(blockchainId)} token={symbol} address={address} size="mui-md" />
<TokenIcon imageBaseUrl={getImageBaseUrl(chain)} token={symbol} address={address} size="mui-md" />
<Typography component="span" variant="bodyMBold">
{symbol}
</Typography>
Expand All @@ -45,12 +45,12 @@ export const LendingMarketsFilters = ({
<Grid container spacing={Spacing.sm} paddingTop={Spacing.sm}>
<Grid size={{ mobile: 12, tablet: 4 }}>
<MultiSelectFilter
field="blockchainId"
renderItem={(blockchainId) => (
field="chain"
renderItem={(chain) => (
<>
<ChainIcon blockchainId={blockchainId} size="md" />
<ChainIcon blockchainId={chain} size="md" />
<Typography component="span" variant="bodyMBold">
{capitalize(blockchainId)}
{capitalize(chain)}
</Typography>
</>
)}
Expand Down Expand Up @@ -79,7 +79,7 @@ export const LendingMarketsFilters = ({

<Grid size={{ mobile: 12, tablet: 6 }}>
<MinimumSliderFilter
field="totalSupplied.usdTotal"
field="liquidityUsd"
title={t`Min Liquidity`}
format={(value) => formatNumber(value, { currency: 'USD' })}
{...props}
Expand Down
122 changes: 21 additions & 101 deletions apps/main/src/loan/components/PageLlamaMarkets/LendingMarketsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,130 +2,41 @@ import Stack from '@mui/material/Stack'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
import { TableFilters, useColumnFilters } from '@ui-kit/shared/ui/TableFilters'
import { t } from '@ui-kit/lib/i18n'
import { CompactUsdCell, LineGraphCell, PoolTitleCell, RateCell, UtilizationCell } from './cells'
import { DataTable } from '@ui-kit/shared/ui/DataTable'
import { LlamaMarket } from '@/loan/entities/llama-markets'
import {
ColumnDef,
createColumnHelper,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table'
import { getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'
import { LendingMarketsFilters } from '@/loan/components/PageLlamaMarkets/LendingMarketsFilters'
import { useSortFromQueryString } from '@ui-kit/hooks/useSortFromQueryString'
import { DeepKeys } from '@tanstack/table-core/build/lib/utils'
import { useVisibilitySettings, VisibilityGroup } from '@ui-kit/shared/ui/TableVisibilitySettingsPopover'
import { useVisibilitySettings } from '@ui-kit/shared/ui/TableVisibilitySettingsPopover'
import { MarketsFilterChips } from '@/loan/components/PageLlamaMarkets/MarketsFilterChips'
import { DEFAULT_SORT, DEFAULT_VISIBILITY, LLAMA_MARKET_COLUMNS } from '@/loan/components/PageLlamaMarkets/columns'

const { ColumnWidth, Spacing, MaxWidth } = SizesAndSpaces

const columnHelper = createColumnHelper<LlamaMarket>()

/** Define a hidden column. */
const hidden = (id: DeepKeys<LlamaMarket>) =>
columnHelper.accessor(id, {
filterFn: (row, columnId, filterValue) => !filterValue?.length || filterValue.includes(row.getValue(columnId)),
meta: { hidden: true },
})

const [borrowChartId, lendChartId] = ['borrowChart', 'lendChart']

/** Columns for the lending markets table. */
const columns = [
columnHelper.accessor('assets', {
header: t`Collateral • Borrow`,
cell: PoolTitleCell,
size: ColumnWidth.lg,
}),
columnHelper.accessor('rates.borrow', {
header: t`7D Avg Borrow Rate`,
cell: (c) => <RateCell market={c.row.original} type="borrow" />,
meta: { type: 'numeric' },
size: ColumnWidth.sm,
}),
columnHelper.accessor('rates.borrow', {
id: borrowChartId,
header: t`7D Borrow Rate Chart`,
cell: (c) => <LineGraphCell market={c.row.original} type="borrow" />,
size: ColumnWidth.md,
enableSorting: false,
}),
columnHelper.accessor('rates.lend', {
header: t`7D Avg Supply Yield`,
cell: (c) => <RateCell market={c.row.original} type="lend" />,
meta: { type: 'numeric' },
size: ColumnWidth.sm,
sortUndefined: 'last',
}),
columnHelper.accessor('rates.lend', {
id: lendChartId,
header: t`7D Supply Yield Chart`,
cell: (c) => <LineGraphCell market={c.row.original} type="lend" />,
size: ColumnWidth.md,
sortUndefined: 'last',
enableSorting: false,
}),
columnHelper.accessor('utilizationPercent', {
header: t`Utilization`,
cell: UtilizationCell,
meta: { type: 'numeric' },
size: ColumnWidth.sm,
}),
columnHelper.accessor('totalSupplied.usdTotal', {
header: () => t`Available Liquidity`,
cell: CompactUsdCell,
meta: { type: 'numeric' },
size: ColumnWidth.sm,
}),
// following columns are used to configure and filter tanstack, but they are displayed together in PoolTitleCell
hidden('blockchainId'),
hidden('assets.collateral.symbol'),
hidden('assets.borrowed.symbol'),
] satisfies ColumnDef<LlamaMarket, any>[]

const DEFAULT_SORT = [{ id: 'totalSupplied.usdTotal', desc: true }]

const DEFAULT_VISIBILITY: VisibilityGroup[] = [
{
label: t`Markets`,
options: [
{ label: t`Available Liquidity`, id: 'totalSupplied.usdTotal', active: true },
{ label: t`Utilization`, id: 'utilizationPercent', active: true },
],
},
{
label: t`Borrow`,
options: [{ label: t`Chart`, id: borrowChartId, active: true }],
},
{
label: t`Lend`,
options: [{ label: t`Chart`, id: lendChartId, active: true }],
},
]
const { Spacing, MaxWidth } = SizesAndSpaces

export const LendingMarketsTable = ({
onReload,
data,
headerHeight,
isError,
}: {
onReload: () => void
data: LlamaMarket[]
headerHeight: string
isError: boolean
}) => {
const [columnFilters, columnFiltersById, setColumnFilter] = useColumnFilters()
const [columnFilters, columnFiltersById, setColumnFilter, resetFilters] = useColumnFilters()
const { columnSettings, columnVisibility, toggleVisibility } = useVisibilitySettings(DEFAULT_VISIBILITY)

const [sorting, onSortingChange] = useSortFromQueryString(DEFAULT_SORT)
const table = useReactTable({
columns,
columns: LLAMA_MARKET_COLUMNS,
data,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: { sorting, columnVisibility, columnFilters },
onSortingChange,
maxMultiSortColCount: 3, // allow 3 columns to be sorted at once
maxMultiSortColCount: 3, // allow 3 columns to be sorted at once while holding shift
})

return (
Expand All @@ -136,16 +47,25 @@ export const LendingMarketsTable = ({
maxWidth: MaxWidth.table,
}}
>
<DataTable table={table} headerHeight={headerHeight} rowHeight="3xl" emptyText={t`No markets found`}>
<DataTable
table={table}
headerHeight={headerHeight}
rowHeight="3xl"
emptyText={isError ? t`Could not load markets` : t`No markets found`}
>
<TableFilters
title={t`Llamalend Markets`}
subtitle={t`Select a market to view more details`}
onReload={onReload}
learnMoreUrl="https://docs.curve.fi/lending/overview/"
visibilityGroups={columnSettings}
toggleVisibility={toggleVisibility}
onResetFilters={resetFilters}
collapsible={
<LendingMarketsFilters columnFilters={columnFiltersById} setColumnFilter={setColumnFilter} data={data} />
}
>
<LendingMarketsFilters columnFilters={columnFiltersById} setColumnFilter={setColumnFilter} data={data} />
<MarketsFilterChips columnFiltersById={columnFiltersById} setColumnFilter={setColumnFilter} />
</TableFilters>
</DataTable>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { t } from '@ui-kit/lib/i18n'
import { HeartIcon } from '@ui-kit/shared/icons/HeartIcon'
import { PointsIcon } from '@ui-kit/shared/icons/PointsIcon'
import { LlamaMarket, LlamaMarketType } from '@/loan/entities/llama-markets'
import Stack from '@mui/material/Stack'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
import { DeepKeys } from '@tanstack/table-core/build/lib/utils'
import { useCallback } from 'react'
import { SelectableChip } from '@ui-kit/shared/ui/SelectableChip'

const { Spacing } = SizesAndSpaces

type LlamaMarketKey = DeepKeys<LlamaMarket>

type ColumnFilterProps = {
columnFiltersById: Record<LlamaMarketKey, unknown>
setColumnFilter: (id: LlamaMarketKey, value: unknown) => void
}

/** Hook for managing a single boolean filter */
function useToggleFilter(key: LlamaMarketKey, { columnFiltersById, setColumnFilter }: ColumnFilterProps) {
const isFiltered = !!columnFiltersById[key]
const toggle = useCallback(
() => setColumnFilter(key, isFiltered ? undefined : true),
[isFiltered, key, setColumnFilter],
)
return [isFiltered, toggle] as const
}

/**
* Hook for managing market type filter
* @returns marketTypes - object with keys for each market type and boolean values indicating if the type is selected
* @returns toggles - object with keys for each market type and functions to toggle the type
*/
function useMarketTypeFilter({ columnFiltersById, setColumnFilter }: ColumnFilterProps) {
const filter = columnFiltersById['type'] as LlamaMarketType[] | undefined
const toggleMarketType = useCallback(
(type: LlamaMarketType) => {
setColumnFilter(
'type',
!filter || filter.includes(type)
? (filter ?? Object.values(LlamaMarketType)).filter((f) => f !== type)
: [...(filter || []), type],
)
},
[filter, setColumnFilter],
)

const marketTypes = {
[LlamaMarketType.Mint]: !filter || filter.includes(LlamaMarketType.Mint),
[LlamaMarketType.Lend]: !filter || filter.includes(LlamaMarketType.Lend),
}
const toggles = {
[LlamaMarketType.Mint]: useCallback(() => toggleMarketType(LlamaMarketType.Mint), [toggleMarketType]),
[LlamaMarketType.Lend]: useCallback(() => toggleMarketType(LlamaMarketType.Lend), [toggleMarketType]),
}
return [marketTypes, toggles] as const
}

export const MarketsFilterChips = (props: ColumnFilterProps) => {
const [favorites, toggleFavorites] = useToggleFilter('isFavorite', props)
const [rewards, toggleRewards] = useToggleFilter('rewards', props)
const [marketTypes, toggleMarkets] = useMarketTypeFilter(props)

return (
<Stack direction="row" gap={Spacing.lg} flexWrap="wrap">
<Stack direction="row" gap="4px">
<SelectableChip
label={t`Favorites`}
selected={favorites}
toggle={toggleFavorites}
icon={<HeartIcon />}
data-testid="chip-favorites"
/>
<SelectableChip
label={t`Rewards`}
selected={rewards}
toggle={toggleRewards}
icon={<PointsIcon />}
data-testid="chip-rewards"
/>
</Stack>
<Stack direction="row" gap="4px">
<SelectableChip
label={t`Mint Markets`}
selected={marketTypes.Mint}
toggle={toggleMarkets.Mint}
data-testid="chip-mint"
/>
<SelectableChip
label={t`Lend Markets`}
selected={marketTypes.Lend}
toggle={toggleMarkets.Lend}
data-testid="chip-lend"
/>
</Stack>
</Stack>
)
}
21 changes: 16 additions & 5 deletions apps/main/src/loan/components/PageLlamaMarkets/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,36 @@ import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
import { useHeaderHeight } from '@ui-kit/widgets/Header'
import useStore from '@/loan/store/useStore'
import { useLlamaMarkets } from '@/loan/entities/llama-markets'
import usePageOnMount from '@/loan/hooks/usePageOnMount'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { invalidateMintMarkets } from '@/loan/entities/mint-markets'

const onReload = () => invalidateLendingVaults({})
/**
* Reloads the lending vaults and mint markets.
* Note: It does not reload the snapshots (for now).
*/
const onReload = () => {
invalidateLendingVaults({})
invalidateMintMarkets({})
}

const { Spacing, MaxWidth, ModalHeight } = SizesAndSpaces

/**
* Page for displaying the lending markets table.
*/
export const PageLlamaMarkets = () => {
const { data, isFetching, isError } = useLlamaMarkets() // todo: show errors and loading state
const { data, isLoading, isError } = useLlamaMarkets() // todo: show errors and loading state
const bannerHeight = useStore((state) => state.layout.height.globalAlert)
const headerHeight = useHeaderHeight(bannerHeight)
usePageOnMount(useParams(), useLocation(), useNavigate()) // required for connecting wallet
return (
<Box sx={{ marginBlockEnd: Spacing.xxl }}>
<DocumentHead title={t`Llamalend Markets`} />
{data ? (
<LendingMarketsTable onReload={onReload} data={data} headerHeight={headerHeight} />
) : (
{isLoading ? (
<Skeleton variant="rectangular" width={MaxWidth.table} height={ModalHeight.md.height} />
) : (
<LendingMarketsTable onReload={onReload} data={data ?? []} headerHeight={headerHeight} isError={isError} />
)}
<LendTableFooter />
</Box>
Expand Down
Loading