Skip to content

Commit 805e147

Browse files
committed
Reimplement top vaults sorting by col-header
1 parent 0ff99dd commit 805e147

2 files changed

Lines changed: 181 additions & 38 deletions

File tree

src/lib/top-vaults/DepositEventsCell.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030

3131
<div class="deposit-events-cell">
3232
<span>{number}</span>
33-
<span class="unit">{unit}</span>
33+
{#if unit}
34+
<span class="unit">{unit}</span>
35+
{/if}
3436
</div>
3537

3638
<style>

src/lib/top-vaults/TopVaultsTable.svelte

Lines changed: 178 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script lang="ts">
2-
import type { TopVaults } from './schemas';
2+
import type { TopVaults, VaultInfo } from './schemas';
33
import type { Chain } from '$lib/helpers/chain';
4-
import { getChain, getExplorerUrl } from '$lib/helpers/chain';
54
import Alert from '$lib/components/Alert.svelte';
65
import CryptoAddressWidget from '$lib/components/CryptoAddressWidget.svelte';
76
import TextInput from '$lib/components/TextInput.svelte';
@@ -11,8 +10,17 @@
1110
import FeesCell from './FeesCell.svelte';
1211
import DepositEventsCell from './DepositEventsCell.svelte';
1312
import RiskCell from './RiskCell.svelte';
13+
import IconChevronUp from '~icons/local/chevron-up';
14+
import IconChevronDown from '~icons/local/chevron-down';
15+
import { getChain, getExplorerUrl } from '$lib/helpers/chain';
1416
import { formatDollar, formatNumber, formatPercent, formatValue } from '$lib/helpers/formatters';
1517
18+
interface SortOptions {
19+
key: string;
20+
direction: 'asc' | 'desc';
21+
compareFn: (a: VaultInfo, b: VaultInfo) => number;
22+
}
23+
1624
interface Props {
1725
topVaults: TopVaults;
1826
chain?: Chain;
@@ -26,7 +34,7 @@
2634
let filterValue = $state('');
2735
2836
// filter vaults
29-
let vaults = $derived.by(() => {
37+
let filteredVaults = $derived.by(() => {
3038
const filterCompareStr = filterValue.trim().toLowerCase();
3139
return topVaults.vaults.filter((v) => {
3240
const chain = getChain(v.chain_id);
@@ -42,8 +50,71 @@
4250
return vaultCompareStr.toLowerCase().includes(filterCompareStr);
4351
});
4452
});
53+
54+
let sortOptions = $state<SortOptions>({
55+
key: 'one_month_return_ann',
56+
direction: 'desc',
57+
compareFn: multiValCompare(['one_month_cagr_net', 'one_month_cagr'])
58+
});
59+
60+
// sort vaults
61+
let sortedVaults = $derived.by(() => {
62+
const sorted = filteredVaults.toSorted(sortOptions.compareFn);
63+
if (sortOptions.direction === 'desc') sorted.reverse();
64+
return sorted;
65+
});
66+
67+
function multiValCompare(keys: (keyof VaultInfo)[], defaultValue = -Infinity) {
68+
return (a: VaultInfo, b: VaultInfo) => {
69+
for (const key of keys) {
70+
const aVal = a[key] as number | null;
71+
const bVal = b[key] as number | null;
72+
if (aVal === bVal) continue;
73+
return (aVal ?? defaultValue) - (bVal ?? defaultValue);
74+
}
75+
return 0;
76+
};
77+
}
78+
79+
function stringCompare(sortBy: (v: VaultInfo) => string) {
80+
return (a: VaultInfo, b: VaultInfo) => {
81+
return sortBy(a).localeCompare(sortBy(b));
82+
};
83+
}
84+
85+
function sortBy(
86+
key: SortOptions['key'],
87+
direction: SortOptions['direction'],
88+
compareFn: (a: VaultInfo, b: VaultInfo) => number
89+
) {
90+
if (sortOptions.key === key) {
91+
direction = sortOptions.direction === 'asc' ? 'desc' : 'asc';
92+
}
93+
sortOptions = { key, direction, compareFn };
94+
}
4595
</script>
4696

97+
{#snippet sortColHeader(
98+
label: string,
99+
key: string,
100+
direction: SortOptions['direction'],
101+
compareFn: SortOptions['compareFn']
102+
)}
103+
<th class={key}>
104+
<button onclick={() => sortBy(key, direction, compareFn)}>
105+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
106+
{@html label}
107+
{#if sortOptions.key === key}
108+
{#if sortOptions.direction === 'asc'}
109+
<IconChevronUp />
110+
{:else}
111+
<IconChevronDown />
112+
{/if}
113+
{/if}
114+
</button>
115+
</th>
116+
{/snippet}
117+
47118
{#snippet multiVal<T = MaybeNumber>(values: [T, T], formatter: Formatter<T>)}
48119
<div class="multiline multival">
49120
<span class="primary">{formatter(values[0])}</span>
@@ -70,25 +141,85 @@
70141
<thead>
71142
<tr>
72143
<th class="index"></th>
73-
<th class="chain"></th>
74-
<th class="vault">Vault</th>
75-
<th class="one_month_return_ann">1M return ann.<br />(net/&ZeroWidthSpace;gross)</th>
76-
<th class="three_months_return_ann">3M return ann.<br />(net/&ZeroWidthSpace;gross)</th>
77-
<th class="lifetime_return_ann">Lifetime return ann.<br />(net/&ZeroWidthSpace;gross)</th>
78-
<th class="lifetime_return_abs">Lifetime return abs.<br />(net/&ZeroWidthSpace;gross)</th>
79-
<th class="three_months_sharpe">3m Sharpe</th>
80-
<th class="three_months_volatility">3M Vola-tility</th>
81-
<th class="denomination">Denom-ination</th>
82-
<th class="tvl">TVL USD<br />(current/&ZeroWidthSpace;peak)</th>
83-
<th class="age">Age (Years)</th>
84-
<th class="fees">Fees<br />(mgmt/&ZeroWidthSpace;perf)</th>
85-
<th class="event_count">Deposit Events</th>
86-
<th class="risk">Protocol Technical Risk</th>
144+
{@render sortColHeader(
145+
'',
146+
'chain',
147+
'asc',
148+
stringCompare((v) => getChain(v.chain_id)?.name ?? `Chain ${v.chain_id}`)
149+
)}
150+
{@render sortColHeader(
151+
'Vault',
152+
'vault',
153+
'asc',
154+
stringCompare((v) => `${v.name.trim()} ${v.protocol}`)
155+
)}
156+
{@render sortColHeader(
157+
'1M return ann.<br/>(net/&ZeroWidthSpace;gross)',
158+
'one_month_return_ann',
159+
'desc',
160+
multiValCompare(['one_month_cagr_net', 'one_month_cagr'])
161+
)}
162+
{@render sortColHeader(
163+
'3M return ann.<br/>(net/&ZeroWidthSpace;gross)',
164+
'three_months_return_ann',
165+
'desc',
166+
multiValCompare(['three_months_cagr_net', 'three_months_cagr'])
167+
)}
168+
{@render sortColHeader(
169+
'Lifetime return ann.<br/>(net/&ZeroWidthSpace;gross)',
170+
'lifetime_return_ann',
171+
'desc',
172+
multiValCompare(['cagr_net', 'cagr'])
173+
)}
174+
{@render sortColHeader(
175+
'Lifetime return abs.<br/>(net/&ZeroWidthSpace;gross)',
176+
'lifetime_return_abs',
177+
'desc',
178+
multiValCompare(['lifetime_return_net', 'lifetime_return'])
179+
)}
180+
{@render sortColHeader(
181+
'3m Sharpe',
182+
'three_months_sharpe',
183+
'desc',
184+
multiValCompare(['three_months_sharpe'])
185+
)}
186+
{@render sortColHeader(
187+
'3M Vola&shy;tility',
188+
'three_months_volatility',
189+
'asc',
190+
multiValCompare(['three_months_volatility'])
191+
)}
192+
{@render sortColHeader(
193+
'Denom&shy;ination',
194+
'denomination',
195+
'asc',
196+
stringCompare((v) => v.denomination)
197+
)}
198+
{@render sortColHeader(
199+
'TVL USD<br/>(current/&ZeroWidthSpace;peak)',
200+
'tvl',
201+
'desc',
202+
multiValCompare(['current_nav', 'peak_nav'])
203+
)}
204+
{@render sortColHeader('Age (Years)', 'age', 'desc', multiValCompare(['years']))}
205+
{@render sortColHeader(
206+
'Fees<br />(mgmt/&ZeroWidthSpace;perf)',
207+
'fees',
208+
'asc',
209+
multiValCompare(['mgmt_fee', 'perf_fee'], Infinity)
210+
)}
211+
{@render sortColHeader('Deposit Events', 'event_count', 'desc', multiValCompare(['event_count']))}
212+
{@render sortColHeader(
213+
'Protocol Technical Risk',
214+
'risk',
215+
'asc',
216+
multiValCompare(['risk_numeric'], Infinity)
217+
)}
87218
<th class="address">Vault Address</th>
88219
</tr>
89220
</thead>
90221
<tbody>
91-
{#each vaults as vault (vault.id)}
222+
{#each sortedVaults as vault (vault.id)}
92223
{@const chain = getChain(vault.chain_id)}
93224
<tr>
94225
<!-- index cell is populated with row index via `rowNumber` CSS counter -->
@@ -229,21 +360,41 @@
229360
background: color-mix(in srgb, var(--c-body), hsl(var(--hsl-box)) var(--box-4-alpha));
230361
/* sticky header border gets lost on scroll, so use box-shadow instead */
231362
box-shadow: inset 0px -2px var(--c-text-extra-light);
232-
padding: 0.5rem;
233363
/* add extra padding to bottom to account for the inset box-shadow */
234-
padding-bottom: calc(0.5rem + 2px);
364+
--th-padding: 0.5rem 0.5rem calc(0.5rem + 2px) 0.5rem;
235365
font-weight: 900;
236366
text-transform: uppercase;
237367
text-align: left;
238-
}
239368

240-
th.sorted {
241-
padding-right: 1.125rem;
369+
&:not(:has(button)) {
370+
padding: var(--th-padding);
371+
}
242372

243-
svg {
244-
position: absolute;
245-
top: 0.5rem;
246-
right: 0.25rem;
373+
button {
374+
display: flex;
375+
border: none;
376+
width: 100%;
377+
min-height: 4.75rem;
378+
padding: var(--th-padding);
379+
background: transparent;
380+
font: inherit;
381+
text-align: inherit;
382+
text-transform: inherit;
383+
cursor: pointer;
384+
}
385+
386+
:global(.icon) {
387+
min-width: 1em;
388+
translate: 0.25rem 0;
389+
390+
:global(*) {
391+
stroke-width: 3;
392+
}
393+
}
394+
395+
/* custom alignment for chain sort indicator (no header label) */
396+
&.chain :global(.icon) {
397+
translate: 0;
247398
}
248399
}
249400

@@ -252,11 +403,6 @@
252403
background: var(--c-body);
253404
}
254405

255-
/* custom alignment for chain sort indicator (no header label) */
256-
th.chain.sorted svg {
257-
right: 0.5rem;
258-
}
259-
260406
td.chain {
261407
width: 1.875rem;
262408
}
@@ -265,11 +411,6 @@
265411
min-width: 12rem;
266412
}
267413

268-
/* flip the sort indicator on columns that use inverted sort */
269-
:is(th.chain, th.vault, th.denomination, th.fees, th.risk) svg {
270-
rotate: 180deg;
271-
}
272-
273414
td {
274415
border-block: 1px solid var(--c-text-ultra-light);
275416
padding: 0.25em 0.5em;

0 commit comments

Comments
 (0)