@@ -4,17 +4,23 @@ import { DateTime } from "luxon"
44
55/** UI */
66import Button from " @/components/ui/Button.vue"
7+ import Checkbox from " @/components/ui/Checkbox.vue"
8+ import Popover from " @/components/ui/Popover.vue"
79import Tooltip from " @/components/ui/Tooltip.vue"
810
911/** Components */
1012import AmountInCurrency from " @/components/AmountInCurrency.vue"
1113
1214/** Services */
13- import { formatBytes , comma , truncateDecimalPart } from " @/services/utils"
15+ import { formatBytes , comma , truncateDecimalPart , capitilize } from " @/services/utils"
1416
1517/** API */
1618import { fetchRollups , fetchRollupsCount } from " @/services/api/rollup"
1719
20+ /** Stores */
21+ import { useEnumStore } from " @/store/enums"
22+ const enumStore = useEnumStore ()
23+
1824useHead ({
1925 title: " Rollups - Celestia Explorer" ,
2026 link: [
@@ -80,6 +86,60 @@ const sort = reactive({
8086 by: " size" ,
8187 dir: " desc" ,
8288})
89+ const categories = computed (() => {
90+ let res = []
91+ if (enumStore .enums .rollupCategories .length ) {
92+ res = enumStore .enums .rollupCategories .slice (1 )
93+ res .push (' other' )
94+ }
95+
96+ return res
97+ })
98+ const filters = reactive ({
99+ categories: categories .value ? .reduce ((a , b ) => ({ ... a, [b]: false }), {}),
100+ })
101+ const savedFiltersBeforeChanges = ref (null )
102+
103+ const isCategoriesPopoverOpen = ref (false )
104+ const handleOpenCategoriesPopover = () => {
105+ isCategoriesPopoverOpen .value = true
106+
107+ if (Object .keys (filters .categories ).find ((f ) => filters .categories [f])) {
108+ savedFiltersBeforeChanges .value = { ... filters .categories }
109+ }
110+ }
111+ const onCategoriesPopoverClose = () => {
112+ isCategoriesPopoverOpen .value = false
113+
114+ if (savedFiltersBeforeChanges .value ) {
115+ filters .categories = savedFiltersBeforeChanges .value
116+ savedFiltersBeforeChanges .value = null
117+ } else {
118+ resetFilters (" categories" )
119+ }
120+ }
121+ const handleApplyCategoriesFilters = () => {
122+ savedFiltersBeforeChanges .value = null
123+ isCategoriesPopoverOpen .value = false
124+
125+ if (page .value !== 1 ) {
126+ page .value = 1
127+ } else {
128+ getRollups ()
129+ }
130+ }
131+
132+ const resetFilters = (target ) => {
133+ Object .keys (filters[target]).forEach ((f ) => {
134+ filters[target][f] = false
135+ })
136+
137+ if (page .value !== 1 ) {
138+ page .value = 1
139+ } else {
140+ getRollups ()
141+ }
142+ }
83143
84144const getRollupsCount = async () => {
85145 const { data: rollupsCount } = await fetchRollupsCount ()
@@ -95,6 +155,12 @@ const getRollups = async () => {
95155 isRefetching .value = true
96156
97157 const data = await fetchRollups ({
158+ categories:
159+ Object .keys (filters .categories ).find ((f ) => filters .categories [f]) &&
160+ Object .keys (filters .categories )
161+ .filter ((f ) => filters .categories [f])
162+ .map (c => c === ' other' ? ' uncategorized' : c)
163+ .join (" ," ),
98164 limit: 20 ,
99165 offset: (page .value - 1 ) * 20 ,
100166 sort: sort .dir ,
@@ -107,16 +173,6 @@ const getRollups = async () => {
107173
108174getRollups ()
109175
110- /** Refetch rollups */
111- watch (
112- () => page .value ,
113- async () => {
114- getRollups ()
115-
116- router .replace ({ query: { page: page .value } })
117- },
118- )
119-
120176const handleSort = (by ) => {
121177 switch (sort .dir ) {
122178 case " desc" :
@@ -130,8 +186,11 @@ const handleSort = (by) => {
130186 }
131187
132188 sort .by = by
133-
134- getRollups ()
189+ if (page .value !== 1 ) {
190+ page .value = 1
191+ } else {
192+ getRollups ()
193+ }
135194}
136195
137196const handlePrev = () => {
@@ -145,6 +204,23 @@ const handleNext = () => {
145204
146205 page .value += 1
147206}
207+
208+ watch (
209+ () => categories .value ,
210+ () => {
211+ filters .categories = categories .value ? .reduce ((a , b ) => ({ ... a, [b]: false }), {})
212+ }
213+ )
214+
215+ /** Refetch rollups */
216+ watch (
217+ () => page .value ,
218+ async () => {
219+ getRollups ()
220+
221+ router .replace ({ query: { page: page .value } })
222+ },
223+ )
148224< / script>
149225
150226< template>
@@ -191,14 +267,58 @@ const handleNext = () => {
191267 < / Flex>
192268 < / Flex>
193269
270+ < Flex align= " center" justify= " between" wrap= " wrap" gap= " 8" : class = " $style.settings" >
271+ < Flex wrap= " wrap" align= " center" gap= " 8" >
272+ < Popover : open= " isCategoriesPopoverOpen" @on- close= " onCategoriesPopoverClose" width= " 200" >
273+ < Button @click= " handleOpenCategoriesPopover" type= " secondary" size= " mini" >
274+ < Icon name= " plus-circle" size= " 12" color= " tertiary" / >
275+ < Text color= " secondary" > Category< / Text >
276+
277+ < template v- if = " Object.keys(filters.categories).find((c) => filters.categories[c])" >
278+ < div : class = " $style.vertical_divider" / >
279+
280+ < Text size= " 12" weight= " 600" color= " primary" style= " text-transform: capitalize" >
281+ {{ Object .keys (filters .categories )
282+ .filter ((c ) => filters .categories [c])
283+ .map (c => c === ' nft' ? c .toUpperCase () : capitilize (c))
284+ .join (" , " )
285+ }}
286+ < / Text >
287+
288+ < Icon @click .stop = " resetFilters('categories', true)" name= " close-circle" size= " 12" color= " secondary" / >
289+ < / template>
290+ < / Button>
291+
292+ < template #content>
293+ < Flex direction= " column" gap= " 12" >
294+ < Text size= " 12" weight= " 500" color= " secondary" > Filter by Category< / Text >
295+
296+ < Flex direction= " column" gap= " 8" : class = " $style.filters_list" >
297+ < Checkbox
298+ v- for = " c in Object.keys(filters.categories)"
299+ v- model= " filters.categories[c]"
300+ >
301+ < Text size= " 12" weight= " 500" color= " primary" >
302+ {{ c === ' nft' ? c .toUpperCase () : capitilize (c) }}
303+ < / Text >
304+ < / Checkbox>
305+ < / Flex>
306+
307+ < Button @click= " handleApplyCategoriesFilters" type= " secondary" size= " mini" wide> Apply< / Button>
308+ < / Flex>
309+ < / template>
310+ < / Popover>
311+ < / Flex>
312+ < / Flex>
313+
194314 < Flex direction= " column" gap= " 16" wide : class = " [$style.table, isRefetching && $style.disabled]" >
195- <div v-if =" rollups.length" :class =" $style.table_scroller" >
315+ < div v- if = " rollups? .length" : class = " $style.table_scroller" >
196316 < table>
197317 < thead>
198318 < tr>
199319 < th>< Text size= " 12" weight= " 600" color= " tertiary" noWrap> #< / Text >< / th>
200320 < th>< Text size= " 12" weight= " 600" color= " tertiary" noWrap> Rollup< / Text >< / th>
201- <th @click =" handleSort('time')" :class =" $style.sortable" >
321+ <!-- < th @click= " handleSort('time')" : class = " $style.sortable" >
202322 < Flex align= " center" gap= " 6" >
203323 < Text size= " 12" weight= " 600" color= " tertiary" noWrap> Last Active< / Text >
204324 < Icon
@@ -209,6 +329,18 @@ const handleNext = () => {
209329 : style= " { transform: `rotate(${sort.dir === 'asc' ? '180' : '0'}deg)` }"
210330 / >
211331 < / Flex>
332+ < / th> -->
333+ < th>
334+ < Flex align= " center" gap= " 6" >
335+ < Text size= " 12" weight= " 600" color= " tertiary" noWrap> Category< / Text >
336+ <!-- < Icon
337+ v- if = " sort.by === 'time'"
338+ name= " chevron"
339+ size= " 12"
340+ color= " secondary"
341+ : style= " { transform: `rotate(${sort.dir === 'asc' ? '180' : '0'}deg)` }"
342+ / > -->
343+ < / Flex>
212344 < / th>
213345 < th @click= " handleSort('size')" : class = " $style.sortable" >
214346 < Flex align= " center" gap= " 6" >
@@ -262,17 +394,47 @@ const handleNext = () => {
262394 < td style= " width: 1px" >
263395 < NuxtLink : to= " `/rollup/${r.slug}`" >
264396 < Flex align= " center" gap= " 8" >
265- <Flex v-if =" r.logo" align =" center" justify =" center" :class =" $style.avatar_container" >
266- <img :src =" r.logo" :class =" $style.avatar_image" />
397+ < Flex v- if = " r.logo" align= " center" : class = " $style.avatar_wrapper" >
398+ < div : class = " $style.avatar_container" >
399+ < img : src= " r.logo" : class = " $style.avatar_image" / >
400+ < / div>
401+
402+ < Tooltip : class = " $style.status_dot_wrapper" >
403+ < div
404+ : class = " $style.status_dot"
405+ : style= " {
406+ background: `${Math.abs(DateTime.fromISO(r.last_message_time).diffNow('days').days) < 1
407+ ? ''
408+ : Math.abs(DateTime.fromISO(r.last_message_time).diffNow('days').days) < 7
409+ ? 'var(--light-orange)'
410+ : 'var(--red)'
411+ }`
412+ }"
413+ / >
414+
415+ < template #content>
416+ < Flex align= " end" gap= " 8" >
417+ < Text size= " 12" weight= " 600" color= " tertiary" > {{
418+ ` Was active
419+ ${ Math .abs (DateTime .fromISO (r .last_message_time ).diffNow (' days' ).days ) < 1
420+ ? ' less than a day ago'
421+ : Math .abs (DateTime .fromISO (r .last_message_time ).diffNow (' days' ).days ) < 7
422+ ? ' more than a day ago'
423+ : ' more than a week ago'
424+ } `
425+ }} < / Text >
426+ < / Flex>
427+ < / template>
428+ < / Tooltip>
267429 < / Flex>
268-
430+
269431 < Text size= " 12" weight= " 600" color= " primary" mono>
270432 {{ r .name }}
271433 < / Text >
272434 < / Flex>
273435 < / NuxtLink>
274436 < / td>
275- <td >
437+ <!-- < td>
276438 < NuxtLink : to= " `/rollup/${r.slug}`" >
277439 < Flex direction= " column" justify= " center" gap= " 4" >
278440 < Text size= " 12" weight= " 600" color= " primary" >
@@ -284,6 +446,13 @@ const handleNext = () => {
284446 < / Text >
285447 < / Flex>
286448 < / NuxtLink>
449+ < / td> -->
450+ < td>
451+ < NuxtLink : to= " `/rollup/${r.slug}`" >
452+ < Flex align= " center" >
453+ < Text size= " 13" weight= " 600" color= " primary" > {{ capitilize (r .category ) }} < / Text >
454+ < / Flex>
455+ < / NuxtLink>
287456 < / td>
288457 < td>
289458 < NuxtLink : to= " `/rollup/${r.slug}`" >
@@ -387,6 +556,23 @@ const handleNext = () => {
387556 padding: 0 16px ;
388557}
389558
559+ .settings {
560+ border- radius: 4px ;
561+ background: var (-- card- background);
562+
563+ padding: 8px 16px ;
564+ }
565+
566+ .vertical_divider {
567+ min- width: 2px ;
568+ height: 12px ;
569+ background: var (-- op- 10 );
570+ }
571+
572+ .filters_list {
573+ height: auto;
574+ }
575+
390576.table_scroller {
391577 overflow- x: auto;
392578}
@@ -469,10 +655,16 @@ const handleNext = () => {
469655 }
470656}
471657
658+ .avatar_wrapper {
659+ position: relative;
660+ width: 25px ;
661+ height: 25px ;
662+ }
663+
472664.avatar_container {
473665 position: relative;
474- width : 25 px ;
475- height : 25 px ;
666+ width: 100 % ;
667+ height: 100 % ;
476668 overflow: hidden;
477669 border- radius: 50 % ;
478670}
@@ -483,6 +675,21 @@ const handleNext = () => {
483675 object- fit: cover;
484676}
485677
678+ .status_dot_wrapper {
679+ position: absolute;
680+ bottom: 0px ;
681+ right: 0px ;
682+ z- index: 1 ;
683+ }
684+
685+ .status_dot {
686+ width: 10px ;
687+ height: 10px ;
688+ border- radius: 50 % ;
689+ background: var (-- brand);
690+ border: 1px solid var (-- card- background);
691+ }
692+
486693.table .disabled {
487694 opacity: 0.5 ;
488695 pointer- events: none;
0 commit comments