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 @@ + + + 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 @@ + + + + + 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() +} 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() +} 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() +} 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() +} 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" > -
-
- -
- - -
+
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)