Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
b55befd
feat: search bar with full width
0xPearce Nov 3, 2025
2037418
feat: onreload user positions
0xPearce Nov 3, 2025
c6415dd
feat: new user positions tabs + llama monitor bot button
0xPearce Nov 3, 2025
050aa9c
feat: user position table with searchbar, collapsible filters, chains…
0xPearce Nov 3, 2025
5e1ac77
feat: removed some filter chips for user positions
0xPearce Nov 3, 2025
ef19ddb
fix: key prop to user position table to force re-render
0xPearce Nov 3, 2025
8b52961
fix: mint and lend markets column filter set to null when array empty
0xPearce Nov 3, 2025
e6f30f9
fix: tabs underline color
0xPearce Nov 7, 2025
14aedc5
feat: end adornment tab with user's market count
0xPearce Nov 7, 2025
a2cc7ca
feat: bell icon to llama monitor bot
0xPearce Nov 7, 2025
6968e8e
Merge branch 'main' into refactor/user-positions-table-llamalend
0xPearce Nov 7, 2025
c1d98ba
feat: render 3 rows and show all user positions button
0xPearce Nov 7, 2025
f2014d1
fix: bell icon svg dimensions
0xPearce Nov 7, 2025
9caf965
feat: llamabot button link for mobile
0xPearce Nov 7, 2025
233c63e
Merge branch 'main' into refactor/user-positions-table-llamalend
0xPearce Nov 7, 2025
e717639
feat: user position empty states
0xPearce Nov 7, 2025
4549076
Initial plan
Copilot Nov 10, 2025
84e9e26
feat: add bell ringing animation on hover
Copilot Nov 10, 2025
86d38bb
refactor: extract BellRingingIcon component with Storybook
Copilot Nov 10, 2025
4160143
Merge pull request #1627 from curvefi/copilot/sub-pr-1600
DanielSchiavini Nov 10, 2025
465fcf2
feat: user position header title
0xPearce Nov 10, 2025
4067dae
fix: bell stories typography and lint
0xPearce Nov 10, 2025
20b12e6
refactor: llamabot link to constants
0xPearce Nov 10, 2025
217d042
fix: use sort from query using field name
0xPearce Nov 10, 2025
4f9161f
refactor: table visible rows native to DataTable
0xPearce Nov 10, 2025
8f89a06
feat: user position connect wallet
0xPearce Nov 10, 2025
02123ad
feat: optional title empty state
0xPearce Nov 10, 2025
29394d9
fix: user position search bar hiding for mobile
0xPearce Nov 10, 2025
debd41a
fix: is user position prop to filter drawer
0xPearce Nov 10, 2025
7172dbc
refactor: user positions empty state to constants file
0xPearce Nov 10, 2025
6b1a2f8
feat: code optimizations + lint
0xPearce Nov 10, 2025
36669ba
Merge branch 'main' into refactor/user-positions-table-llamalend
0xPearce Nov 10, 2025
ad99fce
feat: user position statistics with dummy data
0xPearce Nov 10, 2025
45fd249
refactor: user positions statistics renamed and displayed if wallet p…
0xPearce Nov 10, 2025
9ce26e7
refactor: user position background color
0xPearce Nov 11, 2025
f514df4
fix: llama market chips only for borrow positions
0xPearce Nov 11, 2025
935f27b
feat: removed user positions summary
0xPearce Nov 12, 2025
ded43f0
Merge branch 'main' into refactor/user-positions-table-llamalend
0xPearce Nov 12, 2025
03217de
fix: default tab updates after data load
0xPearce Nov 12, 2025
1a46e82
Merge branch 'main' into refactor/user-positions-table-llamalend
0xPearce Nov 12, 2025
2b93447
refactor: is expanded table search bar
0xPearce Nov 12, 2025
66380cd
fix: llama bot and user position copywriting
0xPearce Nov 14, 2025
edc1704
refacto: useAccount address to check wallet connection
0xPearce Nov 14, 2025
e375f45
refactor: move the view all rows logic to DataTable
0xPearce Nov 14, 2025
d6c621a
chore: removed old code
0xPearce Nov 14, 2025
be614cd
fix: user has positions prop not optional
0xPearce Nov 14, 2025
cba9cb2
Merge branch 'main' into refactor/user-positions-table-llamalend
0xPearce Nov 14, 2025
6cf0b7f
refactor: minor optimizations
0xPearce Nov 17, 2025
d6f7f7f
Merge branch 'main' into refactor/user-positions-table-llamalend
0xPearce Nov 17, 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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const LlamaMarketsList = () => {
const loading = isReloading || (!data && (!isError || isLoading)) // on initial render isLoading is still false
return (
<ListPageWrapper footer={<LendTableFooter />}>
{data?.userHasPositions && <UserPositionsTabs result={data} loading={loading} />}
{data?.userHasPositions && <UserPositionsTabs onReload={onReload} result={data} loading={loading} />}
<LlamaMarketsTable onReload={onReload} result={data} isError={isError} loading={loading} />
</ListPageWrapper>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Button from '@mui/material/Button'
import Link from '@mui/material/Link'
import { t } from '@ui-kit/lib/i18n'
import { ArrowTopRightIcon } from '@ui-kit/shared/icons/ArrowTopRightIcon'

// TODO: move it to constants / links file
const LLAMA_MONITOR_BOT_URL = 'https://t.me/LlamalendMonitorBot'

export const LlamaMonitorBotButton = () => (
<Button
color="ghost"
size="extraSmall"
component={Link}
sx={{ textDecoration: 'underline', textUnderlineOffset: '2px', '&:hover': { textDecoration: 'underline' } }}
endIcon={<ArrowTopRightIcon />}
href={LLAMA_MONITOR_BOT_URL}
target="_blank"
rel="noreferrer noopener"
>
{t`Get notified with llamamonitorbot`}
</Button>
)
68 changes: 54 additions & 14 deletions apps/main/src/llamalend/features/market-list/UserPositionTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,67 @@
import { useState } from 'react'
import type { LlamaMarketsResult } from '@/llamalend/entities/llama-markets'
import { useMemo, useState } from 'react'
import Stack from '@mui/material/Stack'
import { t } from '@ui-kit/lib/i18n'
import { TabsSwitcher, type TabOption } from '@ui-kit/shared/ui/TabsSwitcher'
import { MarketRateType } from '@ui-kit/types/market'
import { LlamaMonitorBotButton } from './LlamaMonitorBotButton'
import { UserPositionsTable, type UserPositionsTableProps } from './UserPositionsTable'

const tabs: TabOption<MarketRateType>[] = [
{ value: MarketRateType.Borrow, label: t`Borrow` },
{ value: MarketRateType.Supply, label: t`Supply` },
]
const getMarketCountLabel = (openPositions: number) => (openPositions > 0 ? '◼︎' + openPositions : '')

/** Show the first tab that has user positions by default, or the first tab if none are found. */
const getDefault = (userHasPositions: LlamaMarketsResult['userHasPositions'] | undefined) =>
tabs.find(({ value }) => userHasPositions?.Lend[value] || userHasPositions?.Mint[value]) ?? tabs[0]
export const UserPositionsTabs = (props: Omit<UserPositionsTableProps, 'tab' | 'openPositionsByMarketType'>) => {
// Calculate total positions across all markets (independent of filters)
const openPositionsCount = useMemo((): Record<MarketRateType, number> => {
const markets = props.result?.markets ?? []
return {
[MarketRateType.Borrow]: markets.filter((market) => market.userHasPositions?.[MarketRateType.Borrow]).length,
[MarketRateType.Supply]: markets.filter((market) => market.userHasPositions?.[MarketRateType.Supply]).length,
}
}, [props.result?.markets])

// Define tabs with position counts
const tabs: TabOption<MarketRateType>[] = useMemo(
() => [
{
value: MarketRateType.Borrow,
label: `${t`Borrowing`} ${getMarketCountLabel(openPositionsCount[MarketRateType.Borrow])}`,
},
{
value: MarketRateType.Supply,
label: `${t`Lending`} ${getMarketCountLabel(openPositionsCount[MarketRateType.Supply])}`,
},
],
[openPositionsCount],
)

// Show the first tab that has user positions by default, or the first tab if none are found
const defaultTab = useMemo(() => {
const userHasPositions = props.result?.userHasPositions
return tabs.find(({ value }) => userHasPositions?.Lend[value] || userHasPositions?.Mint[value]) ?? tabs[0]
}, [props.result?.userHasPositions, tabs])

const [tab, setTab] = useState<MarketRateType>(defaultTab.value)

export const UserPositionsTabs = (props: Omit<UserPositionsTableProps, 'tab'>) => {
const defaultTab = getDefault(props.result?.userHasPositions).value
const [tab, setTab] = useState<MarketRateType>(defaultTab)
return (
<Stack>
<TabsSwitcher value={tab} onChange={setTab} variant="contained" size="medium" options={tabs} />
<UserPositionsTable {...props} tab={tab} />
<Stack
direction="row"
justifyContent="space-between"
// needed for the bottom border to be the same height as the tabs
alignItems="stretch"
sx={{ backgroundColor: (t) => t.design.Layer[1].Fill }}
>
<TabsSwitcher value={tab} onChange={setTab} variant="underlined" size="small" options={tabs} />
<Stack
alignItems="center"
direction="row"
justifyContent="end"
sx={{ flexGrow: 1, borderBottom: (t) => `1px solid ${t.design.Layer[2].Outline}` }}
>
<LlamaMonitorBotButton />
</Stack>
</Stack>
{/* the key is needed to force a re-render when the tab changes, otherwise filters have stale state for few milliseconds */}
<UserPositionsTable key={tab} {...props} tab={tab} />
</Stack>
)
}
47 changes: 30 additions & 17 deletions apps/main/src/llamalend/features/market-list/UserPositionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ import { DataTable } from '@ui-kit/shared/ui/DataTable/DataTable'
import { EmptyStateRow } from '@ui-kit/shared/ui/DataTable/EmptyStateRow'
import { useColumnFilters } from '@ui-kit/shared/ui/DataTable/hooks/useColumnFilters'
import { TableFilters } from '@ui-kit/shared/ui/DataTable/TableFilters'
import { TableFiltersTitles } from '@ui-kit/shared/ui/DataTable/TableFiltersTitles'
import { TableSearchField } from '@ui-kit/shared/ui/DataTable/TableSearchField'
import { MarketRateType } from '@ui-kit/types/market'
import { type LlamaMarketsResult } from '../../entities/llama-markets'
import { UserPositionFilterChips } from './chips/UserPositionFilterChips'
import { ChainFilterChip } from './chips/ChainFilterChip'
import { LlamaListChips } from './chips/LlamaListChips'
import { DEFAULT_SORT_BORROW, DEFAULT_SORT_SUPPLY, LLAMA_MARKET_COLUMNS } from './columns'
import { LlamaMarketColumnId } from './columns.enum'
import { useLlamaTableVisibility } from './hooks/useLlamaTableVisibility'
import { useSearch } from './hooks/useSearch'
import { LendingMarketsFilters } from './LendingMarketsFilters'
import { LlamaMarketExpandedPanel } from './LlamaMarketExpandedPanel'

const { isEqual } = lodash

const LOCAL_STORAGE_KEYS = {
// not using the t`` here as the value is used as a key in the local storage
[MarketRateType.Borrow]: 'My Borrow Positions',
Expand All @@ -36,15 +39,17 @@ const useDefaultUserFilter = (type: MarketRateType) =>
useMemo(() => [{ id: LlamaMarketColumnId.UserHasPositions, value: type }], [type])

export type UserPositionsTableProps = {
onReload: () => void
result: LlamaMarketsResult | undefined
loading: boolean
tab: MarketRateType
}

const migration: MigrationOptions<ColumnFiltersState> = { version: 1 }

export const UserPositionsTable = ({ result, loading, tab }: UserPositionsTableProps) => {
export const UserPositionsTable = ({ onReload, result, loading, tab }: UserPositionsTableProps) => {
const { markets: data = [], userHasPositions } = result ?? {}
const userData = useMemo(() => data.filter((market) => market.userHasPositions?.[tab]), [data, tab])
const defaultFilters = useDefaultUserFilter(tab)
const title = LOCAL_STORAGE_KEYS[tab]
const [columnFilters, columnFiltersById, setColumnFilter, resetFilters] = useColumnFilters(
Expand All @@ -53,19 +58,20 @@ export const UserPositionsTable = ({ result, loading, tab }: UserPositionsTableP
defaultFilters,
)
const [sorting, onSortingChange] = useSortFromQueryString(DEFAULT_SORT[tab], 'userSort')
const { columnSettings, columnVisibility, sortField } = useLlamaTableVisibility(title, sorting, tab)
const { columnSettings, columnVisibility, sortField, toggleVisibility } = useLlamaTableVisibility(title, sorting, tab)
const [expanded, onExpandedChange] = useState<ExpandedState>({})
const [searchText, onSearch] = useSearch(columnFiltersById, setColumnFilter)
const filterProps = { columnFiltersById, setColumnFilter }

const table = useReactTable({
columns: LLAMA_MARKET_COLUMNS,
data,
data: userData,
state: { expanded, sorting, columnVisibility, columnFilters },
onSortingChange,
onExpandedChange,
...getTableOptions(result),
})

const showChips = userHasPositions?.Lend[tab] && userHasPositions?.Mint[tab]
return (
<DataTable
table={table}
Expand All @@ -76,24 +82,31 @@ export const UserPositionsTable = ({ result, loading, tab }: UserPositionsTableP
>
<TableFilters<LlamaMarketColumnId>
filterExpandedKey={title}
leftChildren={<TableFiltersTitles title={t`${title}`} />}
leftChildren={<TableSearchField value={searchText} onChange={onSearch} testId={`${title}-search`} isExpanded />}
loading={loading}
hasSearchBar
onReload={onReload}
visibilityGroups={columnSettings}
toggleVisibility={toggleVisibility}
searchText={searchText}
onSearch={onSearch}
collapsible={
<LendingMarketsFilters data={userData} columnFilters={columnFiltersById} setColumnFilter={setColumnFilter} />
}
chips={
showChips && (
<UserPositionFilterChips
columnFiltersById={columnFiltersById}
setColumnFilter={setColumnFilter}
<>
<ChainFilterChip data={userData} {...filterProps} />
<LlamaListChips
hiddenMarketCount={result ? userData.length - table.getFilteredRowModel().rows.length : undefined}
hasFilters={columnFilters.length > 0 && !isEqual(columnFilters, defaultFilters)}
resetFilters={resetFilters}
userHasPositions={userHasPositions}
tab={tab}
searchText={searchText}
onSearch={onSearch}
testId={title}
onSortingChange={onSortingChange}
sortField={sortField}
data={userData}
isUserPositions
{...filterProps}
/>
)
</>
}
/>
</DataTable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ type LlamaListChipsProps = {
resetFilters: () => void
hasFilters: boolean
children?: ReactNode
userHasPositions: LlamaMarketsResult['userHasPositions'] | undefined
hasFavorites: boolean | undefined
userHasPositions?: LlamaMarketsResult['userHasPositions']
hasFavorites?: boolean
onSortingChange: OnChangeFn<SortingState>
sortField: LlamaMarketColumnId
data: LlamaMarket[]
minLiquidity: number
minLiquidity?: number
isUserPositions?: boolean
} & FilterProps<string>

export const LlamaListChips = ({
Expand All @@ -36,7 +37,8 @@ export const LlamaListChips = ({
onSortingChange,
sortField,
data,
minLiquidity,
minLiquidity = 0,
isUserPositions,
...filterProps
}: LlamaListChipsProps) => {
const isMobile = useIsMobile()
Expand All @@ -58,6 +60,7 @@ export const LlamaListChips = ({
hiddenMarketCount={hiddenMarketCount}
resetFilters={resetFilters}
hasFilters={hasFilters}
isUserPositions
{...filterProps}
/>
</Grid>
Expand All @@ -67,9 +70,11 @@ export const LlamaListChips = ({
<Grid container columnSpacing={Spacing.xs} justifyContent="flex-end" size={{ mobile: 12, tablet: 'auto' }}>
<LlamaListMarketChips {...filterProps} />
</Grid>
<Grid container columnSpacing={Spacing.xs} justifyContent="flex-end" size={{ mobile: 12, tablet: 'auto' }}>
<LlamaListUserChips userHasPositions={userHasPositions} hasFavorites={hasFavorites} {...filterProps} />
</Grid>
{!isUserPositions && (
<Grid container columnSpacing={Spacing.xs} justifyContent="flex-end" size={{ mobile: 12, tablet: 'auto' }}>
<LlamaListUserChips userHasPositions={userHasPositions} hasFavorites={hasFavorites} {...filterProps} />
</Grid>
)}
</>
)}
{hiddenMarketCount != null && !isMobile && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Props = {
hiddenMarketCount?: number
resetFilters: () => void
hasFilters: boolean
isUserPositions?: boolean
} & FilterProps<string>

export const MarketListFilterDrawer = ({
Expand All @@ -34,6 +35,7 @@ export const MarketListFilterDrawer = ({
hiddenMarketCount,
resetFilters,
hasFilters,
isUserPositions,
...filterProps
}: Props) => {
const [open, openDrawer, closeDrawer] = useSwitch(false)
Expand Down Expand Up @@ -65,7 +67,9 @@ export const MarketListFilterDrawer = ({
<DrawerHeader title={t`Popular Filters`} />
<Grid container spacing={Spacing.sm}>
<LlamaListMarketChips {...filterProps} />
<LlamaListUserChips userHasPositions={userHasPositions} hasFavorites={hasFavorites} {...filterProps} />
{!isUserPositions && (
<LlamaListUserChips userHasPositions={userHasPositions} hasFavorites={hasFavorites} {...filterProps} />
)}
</Grid>
<DrawerHeader title={t`Extras Filters`} />
<LendingMarketsFilters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ export function useMarketTypeFilter({ columnFiltersById, setColumnFilter }: Filt
/** Helper function to toggle the market type filter by updating the column filter state */
const toggleMarketType = useCallback(
(type: LlamaMarketType) => {
setColumnFilter(
LlamaMarketColumnId.Type,
filter?.includes(type) ? filter.filter((f) => f !== type) : [...(filter || []), type],
)
const newFilter = filter?.includes(type) ? filter.filter((f) => f !== type) : [...(filter || []), type]
// Remove the filter entirely if the array is empty, otherwise comparaison with default filters is incorrect
setColumnFilter(LlamaMarketColumnId.Type, newFilter.length === 0 ? null : newFilter)
},
[filter, setColumnFilter],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const TableFilters = <ColumnIds extends string>({
<Stack paddingBlockEnd={{ mobile: Spacing.sm.tablet }} paddingBlockStart={{ mobile: Spacing.md.tablet }}>
<Grid container spacing={Spacing.sm} paddingInline={Spacing.md} justifyContent="space-between">
<Fade in={!hideTitle} timeout={Duration.Transition} mountOnEnter unmountOnExit>
<Grid size={{ mobile: 'auto', tablet: 6 }} sx={{ position: hideTitle ? 'absolute' : 'relative' }}>
<Grid size={{ mobile: 'grow', tablet: 6 }} sx={{ position: hideTitle ? 'absolute' : 'relative' }}>
{leftChildren}
</Grid>
</Fade>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const TableSearchField = ({ value, onChange, testId, toggleExpanded, isEx
) : (
<Box
sx={{
width: { mobile: '100%', tablet: '478px' },
width: '100%',
maxWidth: '100%',
}}
>
Expand Down
Loading