From c8b411b75d83dfd7c3599ad56e008f58439a0e2f Mon Sep 17 00:00:00 2001 From: Seranged <80223622+Seranged@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:58:47 +0100 Subject: [PATCH 01/10] feat: add manager profile pages Add profile pages for label entities and link risk manager/curator displays into the new route. --- .../entities/manager/ManagerEntityLink.vue | 86 +++++++ .../entities/vault/SecuritizeVaultItem.vue | 27 +-- components/entities/vault/VaultBorrowItem.vue | 27 +-- components/entities/vault/VaultEarnItem.vue | 34 +-- components/entities/vault/VaultItem.vue | 27 +-- .../overview/SecuritizeVaultOverview.vue | 14 +- .../overview/VaultOverviewBlockGeneral.vue | 19 +- .../earn/VaultOverviewEarnBlockGeneral.vue | 17 +- composables/useEulerManagerProfile.ts | 84 +++++++ pages/managers/[slug].vue | 223 ++++++++++++++++++ tests/utils/manager-profile.test.ts | 58 +++++ utils/manager-profile.ts | 33 +++ 12 files changed, 532 insertions(+), 117 deletions(-) create mode 100644 components/entities/manager/ManagerEntityLink.vue create mode 100644 composables/useEulerManagerProfile.ts create mode 100644 pages/managers/[slug].vue create mode 100644 tests/utils/manager-profile.test.ts create mode 100644 utils/manager-profile.ts diff --git a/components/entities/manager/ManagerEntityLink.vue b/components/entities/manager/ManagerEntityLink.vue new file mode 100644 index 000000000..0cf37c124 --- /dev/null +++ b/components/entities/manager/ManagerEntityLink.vue @@ -0,0 +1,86 @@ + + + diff --git a/components/entities/vault/SecuritizeVaultItem.vue b/components/entities/vault/SecuritizeVaultItem.vue index a5300b8b0..566c90fc2 100644 --- a/components/entities/vault/SecuritizeVaultItem.vue +++ b/components/entities/vault/SecuritizeVaultItem.vue @@ -4,7 +4,6 @@ import { formatAssetValue } from '~/utils/sdk-prices' import { isVaultBlockedByCountry } from '~/composables/useGeoBlock' import { useEulerProductOfVault, useEulerEntitiesOfVault } from '~/composables/useEulerLabels' import { isVaultGovernanceLimited } from '~/utils/eulerLabelsUtils' -import { getEulerLabelEntityLogo } from '~/entities/euler/labels' import { withVaultIntrinsicApy, getVaultIntrinsicApy, getVaultIntrinsicApyInfo } from '~/utils/vault-intrinsic-apy' import { formatNumber, formatCompactUsdValue } from '~/utils/string-utils' import { VaultApyModal, UiModalPreviewTrigger } from '#components' @@ -30,10 +29,6 @@ const entityName = computed(() => { if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}` return `${entities[0].name} & others` }) -const entityLogos = computed(() => { - if (!entityName.value || entities.length === 0) return [] - return entities.map(e => getEulerLabelEntityLogo(e.logo)) -}) const displayName = computed(() => product.name || vault.shares.name) const isGeoBlocked = computed(() => isVaultBlockedByCountry(vault.address)) @@ -198,18 +193,14 @@ watchEffect(async () => { class="flex items-center gap-6" :class="{ 'opacity-20': isGovernanceLimited }" > - - {{ entityName }} + />
{ class="flex items-center gap-8" :class="{ 'opacity-20': isGovernanceLimited }" > - - {{ entityName }}
{ const entityDisplay = computed(() => { const all = getUniqueEntitiesByVaults([pair.collateral, pair.borrow]) - if (all.length === 0) return { name: '', logos: [] } + if (all.length === 0) return { name: '', entities: [] } const name = all.length === 1 ? all[0].name : all.length === 2 @@ -39,7 +38,7 @@ const entityDisplay = computed(() => { : `${all[0].name} & others` return { name, - logos: all.map(e => getEulerLabelEntityLogo(e.logo)), + entities: all, } }) @@ -465,18 +464,14 @@ const linkPath = computed(() => ({ class="flex items-center gap-6" :class="{ 'opacity-20': isAnyGovernanceLimited }" > - - {{ entityDisplay.name }} + />
({ class="flex items-center gap-8" :class="{ 'opacity-20': isAnyGovernanceLimited }" > - - {{ entityDisplay.name }}
{ if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}` return `${entities[0].name} & others` }) -const entityLogos = computed(() => { - if (!entityName.value || entities.length === 0) return [] - return entities.map(e => getEulerLabelEntityLogo(e.logo)) -}) const { getBalance, isLoading: isBalancesLoading } = useWallets() const { settings } = useUserSettings() const enableIntrinsicApy = computed(() => settings.value.enableIntrinsicApy) @@ -327,18 +322,14 @@ const supplyApyModalData = computed(() => ({ v-else-if="entityName" class="flex items-center gap-6" > - - {{ entityName }} + />
({ /> Unknown
- +
{ if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}` return `${entities[0].name} & others` }) -const entityLogos = computed(() => { - if (!entityName.value || entities.length === 0) return [] - return entities.map(e => getEulerLabelEntityLogo(e.logo)) -}) const isEscrow = computed(() => getVaultCategory(vault.address) === 'escrow') const isBorrowable = computed(() => isVaultBorrowable(vault)) const displayName = computed(() => { @@ -299,18 +294,14 @@ watchEffect(async () => { class="flex items-center gap-6" :class="{ 'opacity-20': isGovernanceLimited }" > - - {{ entityName }} + />
{ class="flex items-center gap-8" :class="{ 'opacity-20': isGovernanceLimited }" > - - {{ entityName }}
{ class="flex items-center gap-8" :class="{ 'opacity-20': isGovernanceLimited }" > - - {{ entity.name }}
{ class="flex items-center gap-8" :class="{ 'opacity-20': isGovernanceLimited }" > - - {{ entity.name }} - {{ entity.name }}
diff --git a/components/entities/vault/overview/earn/VaultOverviewEarnBlockGeneral.vue b/components/entities/vault/overview/earn/VaultOverviewEarnBlockGeneral.vue index 9d1434ce0..06aa14552 100644 --- a/components/entities/vault/overview/earn/VaultOverviewEarnBlockGeneral.vue +++ b/components/entities/vault/overview/earn/VaultOverviewEarnBlockGeneral.vue @@ -4,7 +4,6 @@ import { getAddress } from 'viem' import { formatAssetValue } from '~/utils/sdk-prices' import { useEulerEntitiesOfEarnVault, useEulerProductOfVault } from '~/composables/useEulerLabels' -import { getEulerLabelEntityLogo } from '~/entities/euler/labels' import { isVaultBlockedByCountry } from '~/composables/useGeoBlock' import { isEarnVaultDeprecated, getEarnVaultDeprecationReason, getEarnVaultDescription } from '~/utils/eulerLabelsUtils' import { autoLink } from '~/utils/autoLink' @@ -103,21 +102,7 @@ const feeDisplay = computed(() => { :key="idx" class="flex items-center gap-8" > - - {{ entity.name }} - {{ entity.name }} +
) => { + const { entities, products, isReady: labelsReady } = useEulerLabels() + const { + isEVaultUpdating, + isEarnUpdating, + isSecuritizeUpdating, + isEscrowUpdating, + } = useVaults() + const { + getVerifiedEVaults, + getEarnVaults, + getSecuritizeVaults, + } = useVaultRegistry() + const showAllLabelEntries = useShowAllLabelEntries() + + const entity = computed(() => entities[slug.value] ?? null) + + const productEntries = computed(() => + Object.entries(products) + .filter(([, product]) => isEulerLabelProductManagedBy(product, slug.value)) + .map(([key, product]) => ({ key, product })) + .sort((a, b) => a.product.name.localeCompare(b.product.name)), + ) + + const managesEntity = (candidate: EVault | SecuritizeCollateralVault): boolean => + getEntitiesByVault(candidate as EVault).some(manager => + getEulerLabelEntitySlug(entities, manager) === slug.value, + ) + + const managesEarnEntity = (candidate: EulerEarn): boolean => + getEntitiesByEarnVault(candidate).some(manager => + getEulerLabelEntitySlug(entities, manager) === slug.value, + ) + + const evaults = computed(() => + getVerifiedEVaults(showAllLabelEntries.value) + .filter(managesEntity) + .sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)), + ) + + const securitizeVaults = computed(() => + getSecuritizeVaults() + .filter(managesEntity) + .sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)), + ) + + const earnVaults = computed(() => + getEarnVaults() + .filter(managesEarnEntity) + .sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)), + ) + + const vaultCount = computed(() => + evaults.value.length + securitizeVaults.value.length + earnVaults.value.length, + ) + + const isLoading = computed(() => + !labelsReady.value + || isEVaultUpdating.value + || isEarnUpdating.value + || isSecuritizeUpdating.value + || isEscrowUpdating.value, + ) + + return { + entity, + productEntries, + evaults, + securitizeVaults, + earnVaults, + vaultCount, + isLoading, + } +} diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue new file mode 100644 index 000000000..aa962c9df --- /dev/null +++ b/pages/managers/[slug].vue @@ -0,0 +1,223 @@ + + + diff --git a/tests/utils/manager-profile.test.ts b/tests/utils/manager-profile.test.ts new file mode 100644 index 000000000..108e9837c --- /dev/null +++ b/tests/utils/manager-profile.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' +import type { EulerLabelEntity, EulerLabelProduct } from '~/entities/euler/labels' +import { + getEulerLabelEntityDisplayName, + getEulerLabelEntityKeys, + getEulerLabelEntitySlug, + getManagerProfilePath, + isEulerLabelProductManagedBy, +} from '~/utils/manager-profile' + +const entity = (name: string, logo = `${name}.svg`): EulerLabelEntity => ({ + name, + logo, + description: '', + url: `https://${name.toLowerCase()}.example`, + addresses: {}, + social: { + twitter: '', + youtube: '', + discord: '', + telegram: '', + github: '', + }, +}) + +describe('manager profile helpers', () => { + it('normalizes product entity keys', () => { + expect(getEulerLabelEntityKeys({ entity: 'k3' } as EulerLabelProduct)).toEqual(['k3']) + expect(getEulerLabelEntityKeys({ entity: ['k3', 're7'] } as EulerLabelProduct)).toEqual(['k3', 're7']) + expect(getEulerLabelEntityKeys({ entity: '' } as EulerLabelProduct)).toEqual([]) + }) + + it('detects products managed by a slug', () => { + const product = { entity: ['k3', 're7'] } as EulerLabelProduct + + expect(isEulerLabelProductManagedBy(product, 're7')).toBe(true) + expect(isEulerLabelProductManagedBy(product, 'mev-capital')).toBe(false) + }) + + it('resolves an entity slug by reference or stable fields', () => { + const k3 = entity('K3') + const entities = { k3, re7: entity('Re7') } + + expect(getEulerLabelEntitySlug(entities, k3)).toBe('k3') + expect(getEulerLabelEntitySlug(entities, { ...k3 })).toBe('k3') + }) + + it('formats compact manager labels', () => { + expect(getEulerLabelEntityDisplayName([])).toBe('') + expect(getEulerLabelEntityDisplayName([entity('K3')])).toBe('K3') + expect(getEulerLabelEntityDisplayName([entity('K3'), entity('Re7')])).toBe('K3 & Re7') + expect(getEulerLabelEntityDisplayName([entity('K3'), entity('Re7'), entity('MEV Capital')])).toBe('K3 & others') + }) + + it('builds manager profile paths', () => { + expect(getManagerProfilePath('mev-capital')).toBe('/managers/mev-capital') + }) +}) diff --git a/utils/manager-profile.ts b/utils/manager-profile.ts new file mode 100644 index 000000000..e0053d672 --- /dev/null +++ b/utils/manager-profile.ts @@ -0,0 +1,33 @@ +import type { EulerLabelEntity, EulerLabelProduct } from '~/entities/euler/labels' + +export const getEulerLabelEntityKeys = (product: EulerLabelProduct): string[] => { + if (Array.isArray(product.entity)) return product.entity + return product.entity ? [product.entity] : [] +} + +export const isEulerLabelProductManagedBy = (product: EulerLabelProduct, slug: string): boolean => + getEulerLabelEntityKeys(product).includes(slug) + +export const getEulerLabelEntitySlug = ( + entities: Record, + entity: EulerLabelEntity, +): string => { + const exact = Object.entries(entities).find(([, value]) => value === entity) + if (exact) return exact[0] + + const byStableFields = Object.entries(entities).find(([, value]) => + value.name === entity.name + && value.logo === entity.logo + && value.url === entity.url, + ) + return byStableFields?.[0] ?? '' +} + +export const getEulerLabelEntityDisplayName = (entities: EulerLabelEntity[]): string => { + if (entities.length === 0) return '' + if (entities.length === 1) return entities[0].name + if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}` + return `${entities[0].name} & others` +} + +export const getManagerProfilePath = (slug: string): string => `/managers/${slug}` From 8ae47ffa1cb5b751762404386626ae8c05c0c834 Mon Sep 17 00:00:00 2001 From: Seranged <80223622+Seranged@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:00:22 +0100 Subject: [PATCH 02/10] fix: make manager links keyboard accessible Add keyboard semantics to the span-based manager profile affordance used inside vault cards. --- components/entities/manager/ManagerEntityLink.vue | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/components/entities/manager/ManagerEntityLink.vue b/components/entities/manager/ManagerEntityLink.vue index 0cf37c124..06fe48872 100644 --- a/components/entities/manager/ManagerEntityLink.vue +++ b/components/entities/manager/ManagerEntityLink.vue @@ -46,6 +46,13 @@ const goToManager = () => { if (!isLinked.value) return void navigateTo(to.value) } + +const onSpanKeydown = (event: KeyboardEvent) => { + if (event.key !== 'Enter' && event.key !== ' ') return + event.preventDefault() + event.stopPropagation() + goToManager() +}