diff --git a/components/entities/portfolio/PortfolioEmptyState.vue b/components/entities/portfolio/PortfolioEmptyState.vue
new file mode 100644
index 00000000..d9550ff3
--- /dev/null
+++ b/components/entities/portfolio/PortfolioEmptyState.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+ {{ text }}
+
+
diff --git a/components/ui/UiCustomFilterChips.vue b/components/ui/UiCustomFilterChips.vue
index 8d76b211..d6bd38ec 100644
--- a/components/ui/UiCustomFilterChips.vue
+++ b/components/ui/UiCustomFilterChips.vue
@@ -16,8 +16,11 @@ const emit = defineEmits<{
{{ filter.label }}
diff --git a/components/ui/UiEmptyState.vue b/components/ui/UiEmptyState.vue
new file mode 100644
index 00000000..d8c1f5bf
--- /dev/null
+++ b/components/ui/UiEmptyState.vue
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+ {{ description }}
+
+
+
+
+
+
+
+
+
+
diff --git a/components/ui/UiEmptyStateIcon.vue b/components/ui/UiEmptyStateIcon.vue
new file mode 100644
index 00000000..70501c76
--- /dev/null
+++ b/components/ui/UiEmptyStateIcon.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/components/ui/modals/customFilter.types.ts b/components/ui/modals/customFilter.types.ts
index 4366233e..65eddbc4 100644
--- a/components/ui/modals/customFilter.types.ts
+++ b/components/ui/modals/customFilter.types.ts
@@ -13,5 +13,6 @@ export interface CustomFilter {
operator: 'gt' | 'lt'
value: number
label: string
+ tone?: 'active' | 'neutral'
includeWhenValueUnavailable?: boolean
}
diff --git a/composables/useCustomFilters.ts b/composables/useCustomFilters.ts
index cb0769d3..b005db1c 100644
--- a/composables/useCustomFilters.ts
+++ b/composables/useCustomFilters.ts
@@ -10,18 +10,29 @@ export const useCustomFilters =
(
initialFilters: CustomFilter[] = [],
) => {
const modal = useModal()
- const customFilters = ref([])
+ const customFilters = ref([...initialFilters])
const addCustomFilter = (filter: CustomFilter) => {
- customFilters.value = [...customFilters.value, filter]
+ const filtersWithoutReplacedDefaults = customFilters.value.filter(existing =>
+ !(existing.tone === 'neutral' && existing.metric === filter.metric),
+ )
+ customFilters.value = [...filtersWithoutReplacedDefaults, filter]
}
const removeCustomFilter = (id: string) => {
- customFilters.value = customFilters.value.filter(f => f.id !== id)
+ const removedFilter = customFilters.value.find(f => f.id === id)
+ const nextFilters = customFilters.value.filter(f => f.id !== id)
+ const neutralDefault = removedFilter && removedFilter.tone !== 'neutral'
+ ? initialFilters.find(f => f.tone === 'neutral' && f.metric === removedFilter.metric)
+ : undefined
+
+ customFilters.value = neutralDefault && !nextFilters.some(f => f.metric === neutralDefault.metric)
+ ? [...nextFilters, neutralDefault]
+ : nextFilters
}
const clearCustomFilters = () => {
- customFilters.value = []
+ customFilters.value = [...initialFilters]
}
const openCustomFilterModal = () => {
@@ -34,9 +45,8 @@ export const useCustomFilters = (
}
const matchesCustomFilters = (item: T): boolean => {
- const filters = [...initialFilters, ...customFilters.value]
- if (!filters.length) return true
- return filters.every((f) => {
+ if (!customFilters.value.length) return true
+ return customFilters.value.every((f) => {
const val = getValue(item, f.metric)
if (typeof val !== 'number' || !Number.isFinite(val)) return f.includeWhenValueUnavailable === true
return f.operator === 'gt' ? val > f.value : val < f.value
diff --git a/pages/borrow/index.vue b/pages/borrow/index.vue
index f23de573..78d86bf2 100644
--- a/pages/borrow/index.vue
+++ b/pages/borrow/index.vue
@@ -167,6 +167,7 @@ const defaultBorrowLiquidityFilter = {
operator: 'gt',
value: MIN_BORROW_LIQUIDITY_USD,
label: `Avail. liquidity > ${formatCompactUsdValue(MIN_BORROW_LIQUIDITY_USD)}`,
+ tone: 'neutral',
includeWhenValueUnavailable: true,
} as const
@@ -569,6 +570,34 @@ const sortedBorrowList = computed(() => {
const directed = sortDir.value === 'asc' ? [...sorted].reverse() : sorted
return applyDeprecatedPairSort(directed)
})
+
+const hasDefaultBorrowLiquidityFilter = computed(() =>
+ customFilters.value.some(filter => filter.id === defaultBorrowLiquidityFilter.id),
+)
+const hasClearableFilters = computed(() =>
+ searchQuery.value.trim().length > 0
+ || selectedCollateral.value.length > 0
+ || selectedDebt.value.length > 0
+ || selectedMarkets.value.length > 0
+ || selectedRiskManagers.value.length > 0
+ || customFilters.value.some(filter => filter.id !== defaultBorrowLiquidityFilter.id),
+)
+const hasBorrowMarkets = computed(() => activeBorrowList.value.length > 0)
+const emptyStateTitle = computed(() => hasClearableFilters.value || hasBorrowMarkets.value ? 'No borrow markets found' : 'No borrow markets yet')
+const emptyStateDescription = computed(() => {
+ if (hasClearableFilters.value) return 'Try clearing search or filters to uncover more borrow pairs.'
+ if (hasDefaultBorrowLiquidityFilter.value && hasBorrowMarkets.value) return 'No borrow pairs match the visible filters.'
+ return 'No borrow markets are available on this network yet.'
+})
+
+const clearBorrowFilters = () => {
+ clearSearch()
+ selectedCollateral.value = []
+ selectedDebt.value = []
+ selectedMarkets.value = []
+ selectedRiskManagers.value = []
+ clearCustomFilters()
+}
@@ -679,18 +708,26 @@ const sortedBorrowList = computed(() => {
:items="sortedBorrowList"
/>
-
-
-
- No markets were found by these filters
-
-
+
+
+ Clear filters
+
+
+
diff --git a/pages/earn/index.vue b/pages/earn/index.vue
index 3a0a9120..3b03cdc9 100644
--- a/pages/earn/index.vue
+++ b/pages/earn/index.vue
@@ -224,6 +224,28 @@ const sortedList = computed(() => {
const directed = sortDir.value === 'asc' ? [...sorted].reverse() : sorted
return applyDeprecatedSort(directed)
})
+
+const hasActiveFilters = computed(() =>
+ searchQuery.value.trim().length > 0
+ || selectedCollateral.value.length > 0
+ || selectedCurators.value.length > 0
+ || customFilters.value.length > 0,
+)
+const hasEarnVaults = computed(() => list.value.length > 0)
+const showFilteredEmptyState = computed(() => hasActiveFilters.value && hasEarnVaults.value)
+const emptyStateTitle = computed(() => showFilteredEmptyState.value ? 'No earn vaults found' : 'No earn vaults yet')
+const emptyStateDescription = computed(() =>
+ showFilteredEmptyState.value
+ ? 'Try clearing search or filters to uncover more strategies.'
+ : 'No earn vaults are available on this network yet.',
+)
+
+const clearEarnFilters = () => {
+ clearSearch()
+ selectedCollateral.value = []
+ selectedCurators.value = []
+ clearCustomFilters()
+}
@@ -300,18 +322,26 @@ const sortedList = computed(() => {
:items="sortedList"
/>
-
-
-
- No markets were found by these filters
-
-
+
+
+ Clear filters
+
+
+
diff --git a/pages/explore/index.vue b/pages/explore/index.vue
index 1e00b0ea..561fc734 100644
--- a/pages/explore/index.vue
+++ b/pages/explore/index.vue
@@ -289,6 +289,30 @@ const isLoading = computed(() =>
|| isTokenListLoading.value || !marketGroupsReady.value || isResolvingTVL.value,
)
const { isSlow } = useSlowLoading(isLoading)
+
+const hasActiveFilters = computed(() =>
+ searchQuery.value.trim().length > 0
+ || selectedMarkets.value.length > 0
+ || selectedAssets.value.length > 0
+ || selectedRiskManagers.value.length > 0
+ || customFilters.value.length > 0,
+)
+const hasExploreMarkets = computed(() => marketGroups.value.some(group => group.source === 'product'))
+const showFilteredEmptyState = computed(() => hasActiveFilters.value && hasExploreMarkets.value)
+const emptyStateTitle = computed(() => showFilteredEmptyState.value ? 'No markets found' : 'No markets yet')
+const emptyStateDescription = computed(() =>
+ showFilteredEmptyState.value
+ ? 'Try clearing search or filters to uncover more markets.'
+ : 'No markets are available on this network yet.',
+)
+
+const clearExploreFilters = () => {
+ clearSearch()
+ selectedMarkets.value = []
+ selectedAssets.value = []
+ selectedRiskManagers.value = []
+ clearCustomFilters()
+}
@@ -377,18 +401,26 @@ const { isSlow } = useSlowLoading(isLoading)
:markets="sortedMarkets"
/>
-
-
-
- No markets were found by these filters
-
-
+
+
+ Clear filters
+
+
+
diff --git a/pages/lend/index.vue b/pages/lend/index.vue
index 8ed3b012..b55fc40e 100644
--- a/pages/lend/index.vue
+++ b/pages/lend/index.vue
@@ -277,6 +277,30 @@ const sortedList = computed(() => {
const directed = sortDir.value === 'asc' ? [...sorted].reverse() : sorted
return applyDeprecatedSort(directed)
})
+
+const hasActiveFilters = computed(() =>
+ searchQuery.value.trim().length > 0
+ || selectedCollateral.value.length > 0
+ || selectedMarkets.value.length > 0
+ || selectedRiskManagers.value.length > 0
+ || customFilters.value.length > 0,
+)
+const hasLendMarkets = computed(() => borrowableVaults.value.length > 0)
+const showFilteredEmptyState = computed(() => hasActiveFilters.value && hasLendMarkets.value)
+const emptyStateTitle = computed(() => showFilteredEmptyState.value ? 'No lend markets found' : 'No lend markets yet')
+const emptyStateDescription = computed(() =>
+ showFilteredEmptyState.value
+ ? 'Try clearing search or filters to uncover more supply opportunities.'
+ : 'No lend markets are available on this network yet.',
+)
+
+const clearLendFilters = () => {
+ clearSearch()
+ selectedCollateral.value = []
+ selectedMarkets.value = []
+ selectedRiskManagers.value = []
+ clearCustomFilters()
+}
@@ -363,18 +387,26 @@ const sortedList = computed(() => {
:items="sortedList"
/>
-
-
-
- No markets were found by these filters
-
-
+
+
+ Clear filters
+
+
+
diff --git a/pages/portfolio/index.vue b/pages/portfolio/index.vue
index c84dbeac..56ce3d98 100644
--- a/pages/portfolio/index.vue
+++ b/pages/portfolio/index.vue
@@ -49,17 +49,11 @@ usePortfolioBatchScrollTarget(computed(() =>
v-else-if="sortedBorrowPositions.length === 0"
class="flex flex-1 justify-center items-center"
>
-
-
-
-
-
- You don't have positions yet
-
-
- Connect your wallet to see your positions
-
-
+
sortRewardsByUsd(rewards.value.filter(
const sortedBrevisRewards = computed(() => sortRewardsByUsd(rewards.value.filter(reward => reward.provider === 'brevis')))
const sortedFuulRewards = computed(() => sortRewardsByUsd(rewards.value.filter(reward => reward.provider === 'fuul')))
const sortedTurtleRewards = computed(() => sortRewardsByUsd(rewards.value.filter(reward => reward.provider === 'turtle')))
+const hasActiveSession = computed(() => isConnected.value || isSpyMode.value)
@@ -43,17 +44,11 @@ const sortedTurtleRewards = computed(() => sortRewardsByUsd(rewards.value.filter
v-else-if="sortedMerklRewards.length === 0"
class="flex flex-1 min-h-[100px] justify-center items-center py-32"
>
-
-
-
-
-
- You don't have rewards yet
-
-
- Connect your wallet to see your rewards
-
-
+
sortRewardsByUsd(rewards.value.filter
v-else-if="sortedBrevisRewards.length === 0"
class="flex flex-1 min-h-[100px] justify-center items-center py-32"
>
-
-
-
-
-
- You don't have rewards yet
-
-
- Connect your wallet to see your rewards
-
-
+
sortRewardsByUsd(rewards.value.filter
v-else-if="sortedFuulRewards.length === 0"
class="flex flex-1 min-h-[100px] justify-center items-center py-32"
>
-
-
-
-
-
- You don't have rewards yet
-
-
- Connect your wallet to see your rewards
-
-
+
sortRewardsByUsd(rewards.value.filter
v-else-if="sortedTurtleRewards.length === 0"
class="flex flex-1 min-h-[100px] justify-center items-center py-32"
>
-
-
-
-
-
- You don't have rewards yet
-
-
- Connect your wallet to see your rewards
-
-
+
sortRewardsByUsd(rewards.value.filter
v-else-if="locks.length === 0"
class="flex flex-1 min-h-[100px] justify-center items-center"
>
-
-
-
-
-
- You don't have locks yet
-
-
- Connect your wallet to see your locks
-
-
+
isConnected.value || isSpyMode.value)
const displayedDepositPositions = computed(() => [
...removedDepositPositions.value,
@@ -62,7 +64,7 @@ usePortfolioBatchScrollTarget(computed(() => [
@@ -71,17 +73,11 @@ usePortfolioBatchScrollTarget(computed(() => [
v-else-if="earnItems.length === 0"
class="flex flex-1 justify-center items-center"
>
-
-
-
-
-
- You don't have deposit positions yet
-
-
- Connect your wallet to see your deposit positions
-
-
+
[
@@ -119,17 +115,11 @@ usePortfolioBatchScrollTarget(computed(() => [
v-else-if="lendItems.length === 0"
class="flex flex-1 justify-center items-center"
>
-
-
-
-
-
- You don't have deposit positions yet
-
-
- Connect your wallet to see your deposit positions
-
-
+