Skip to content

Commit 5ac2416

Browse files
authored
feat: leaderboard collection filter (#1446)
Signed-off-by: Gašper Grom <[email protected]>
1 parent f3e4a3a commit 5ac2416

File tree

7 files changed

+258
-37
lines changed

7 files changed

+258
-37
lines changed

frontend/app/components/modules/collection/services/collections.api.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ class CollectionsApiService {
8888
});
8989
}
9090

91+
searchCollections(query: string) {
92+
return $fetch(`/api/collection`, {
93+
params: {
94+
page: 0,
95+
pageSize: 100,
96+
search: query,
97+
},
98+
});
99+
}
100+
91101
fetchCollectionsQueryFn(
92102
query: () => Record<string, string | number | string[] | undefined>,
93103
): QueryFunction<Pagination<Collection>, readonly unknown[], number> {
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<!--
2+
Copyright (c) 2025 The Linux Foundation and each contributor.
3+
SPDX-License-Identifier: MIT
4+
-->
5+
<template>
6+
<lfx-dropdown-select
7+
v-model="selectedCollection"
8+
:width="props.width"
9+
:match-width="props.matchWidth"
10+
dropdown-class="max-h-80"
11+
placement="bottom-end"
12+
>
13+
<template #trigger="{ selectedOption }">
14+
<lfx-dropdown-selector
15+
:size="props.size"
16+
:type="props.type"
17+
class="!rounded-full flex items-center justify-center w-full"
18+
>
19+
<div class="flex items-center gap-2">
20+
<lfx-icon
21+
name="rectangle-history"
22+
:size="16"
23+
/>
24+
<span class="text-sm text-neutral-900 truncate">
25+
{{ selectedOption.label || 'All collections' }}
26+
</span>
27+
</div>
28+
</lfx-dropdown-selector>
29+
</template>
30+
31+
<template #default>
32+
<div class="sticky -top-1 z-10 bg-white w-full -mt-1 pt-1">
33+
<!-- All collections option -->
34+
<lfx-dropdown-item
35+
value="all"
36+
label="All collections"
37+
/>
38+
39+
<lfx-dropdown-separator />
40+
41+
<!-- Search input -->
42+
<lfx-dropdown-search
43+
v-model="searchQuery"
44+
placeholder="Search collections..."
45+
lazy
46+
class=""
47+
/>
48+
49+
<lfx-dropdown-separator />
50+
</div>
51+
52+
<!-- Collections list -->
53+
<div
54+
v-if="isPending"
55+
class="py-8 flex justify-center"
56+
>
57+
<lfx-spinner />
58+
</div>
59+
60+
<div
61+
v-else-if="!collections.length && searchQuery"
62+
class="py-4 px-3 text-sm text-neutral-500 text-center"
63+
>
64+
No collections found
65+
</div>
66+
67+
<template v-else>
68+
<lfx-dropdown-item
69+
v-for="collection in collections"
70+
:key="collection.id"
71+
:value="collection.slug"
72+
:label="collection.name"
73+
/>
74+
</template>
75+
</template>
76+
</lfx-dropdown-select>
77+
</template>
78+
79+
<script setup lang="ts">
80+
import { computed, ref, watch } from 'vue';
81+
import LfxDropdownSelect from '~/components/uikit/dropdown/dropdown-select.vue';
82+
import LfxDropdownSelector from '~/components/uikit/dropdown/dropdown-selector.vue';
83+
import LfxDropdownItem from '~/components/uikit/dropdown/dropdown-item.vue';
84+
import LfxDropdownSearch from '~/components/uikit/dropdown/dropdown-search.vue';
85+
import LfxDropdownSeparator from '~/components/uikit/dropdown/dropdown-separator.vue';
86+
import LfxIcon from '~/components/uikit/icon/icon.vue';
87+
import type { Collection } from '~~/types/collection';
88+
import type { Pagination } from '~~/types/shared/pagination';
89+
import { COLLECTIONS_API_SERVICE } from '~/components/modules/collection/services/collections.api.service';
90+
import LfxSpinner from '~/components/uikit/spinner/spinner.vue';
91+
92+
const props = withDefaults(
93+
defineProps<{
94+
modelValue?: string;
95+
width?: string;
96+
matchWidth?: boolean;
97+
size?: 'medium' | 'small';
98+
type?: 'transparent' | 'filled';
99+
}>(),
100+
{
101+
modelValue: '',
102+
width: '350px',
103+
matchWidth: false,
104+
size: 'medium',
105+
type: 'transparent',
106+
},
107+
);
108+
109+
const emit = defineEmits<{
110+
(e: 'update:modelValue', value: string): void;
111+
}>();
112+
113+
const selectedCollection = computed({
114+
get: () => props.modelValue,
115+
set: (value: string) => emit('update:modelValue', value),
116+
});
117+
118+
const searchQuery = ref('');
119+
const collections = ref<Collection[]>([]);
120+
const isPending = ref(false);
121+
122+
// Fetch collections from API
123+
const fetchCollections = async () => {
124+
isPending.value = true;
125+
try {
126+
const response = await COLLECTIONS_API_SERVICE.searchCollections(searchQuery.value);
127+
collections.value = (response as Pagination<Collection>).data || [];
128+
} catch (error) {
129+
console.error('Error fetching collections:', error);
130+
collections.value = [];
131+
} finally {
132+
isPending.value = false;
133+
}
134+
};
135+
136+
// Watch for search query changes
137+
watch(
138+
() => searchQuery.value,
139+
() => {
140+
fetchCollections();
141+
},
142+
{ immediate: true },
143+
);
144+
</script>
145+
146+
<script lang="ts">
147+
export default {
148+
name: 'LfxCollectionsFilter',
149+
};
150+
</script>

frontend/app/components/modules/leaderboards/components/sections/leaderboard-detail-header.vue

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Copyright (c) 2025 The Linux Foundation and each contributor.
33
SPDX-License-Identifier: MIT
44
-->
55
<template>
6-
<div class="flex flex-col gap-6 items-start w-full pb-6">
6+
<div class="flex flex-col gap-3 md:gap-6 items-start w-full pb-6">
77
<div
88
v-if="scrollTop < scrollThreshold"
99
class="md:hidden block"
@@ -20,8 +20,12 @@ SPDX-License-Identifier: MIT
2020
</router-link>
2121
</div>
2222
<!-- Icon and Share button -->
23-
<div class="flex justify-between w-full items-start">
24-
<div class="flex transition-all ease-linear flex-col gap-3">
23+
24+
<div class="w-full">
25+
<div
26+
class="flex justify-between w-full items-start"
27+
:class="[scrollTop > scrollThreshold ? '' : 'pb-3']"
28+
>
2529
<div
2630
:class="[scrollTop > scrollThreshold ? 'size-10' : 'size-12']"
2731
class="transition-all ease-linear bg-white border border-neutral-200 rounded-lg flex items-center justify-center"
@@ -32,30 +36,13 @@ SPDX-License-Identifier: MIT
3236
:size="scrollTop > scrollThreshold ? 20 : 24"
3337
/>
3438
</div>
35-
36-
<div class="flex flex-col gap-1">
37-
<h1
38-
:class="[scrollTop > scrollThreshold ? 'text-2xl ml-13 -mt-12' : 'text-3xl']"
39-
class="transition-all ease-linear font-light font-secondary text-neutral-900 md:block hidden"
40-
>
41-
{{ config?.name }}
42-
</h1>
43-
<!-- mobile navigation -->
44-
<div
45-
class="md:hidden flex justify-start"
46-
:class="[scrollTop > scrollThreshold ? 'ml-13 -mt-12' : '']"
47-
>
48-
<lfx-leaderboard-mobile-nav :leaderboard-key="config.key" />
49-
</div>
50-
</div>
51-
</div>
52-
<div class="mt-1">
5339
<div class="md:block hidden">
5440
<lfx-button
55-
type="tertiary"
41+
type="ghost"
5642
size="small"
5743
button-style="pill"
5844
class="h-9"
45+
:class="[scrollTop > scrollThreshold ? 'invisible' : 'visible']"
5946
@click="handleShare"
6047
>
6148
<lfx-icon
@@ -68,19 +55,51 @@ SPDX-License-Identifier: MIT
6855
<div class="md:!hidden block">
6956
<lfx-icon-button
7057
icon="share-nodes"
58+
type="transparent"
7159
@click="handleShare"
7260
/>
7361
</div>
7462
</div>
63+
<div
64+
class="hidden justify-between w-full items-center md:flex"
65+
:class="[scrollTop > scrollThreshold ? '' : 'pb-2']"
66+
>
67+
<h1
68+
:class="[scrollTop > scrollThreshold ? 'text-2xl ml-13 -mt-10' : 'text-3xl']"
69+
class="transition-all ease-linear font-light font-secondary text-neutral-900 md:block hidden"
70+
>
71+
{{ config?.name }}
72+
</h1>
73+
<lfx-collections-filter
74+
v-model="selectedCollection"
75+
width="350px"
76+
:size="scrollTop < scrollThreshold ? 'medium' : 'small'"
77+
type="filled"
78+
class="transition-all ease-linear"
79+
:class="[scrollTop > scrollThreshold ? '-mt-10' : '']"
80+
/>
81+
</div>
82+
<div
83+
class="md:hidden flex justify-start transition-all ease-linear"
84+
:class="[scrollTop > scrollThreshold ? 'ml-13 -mt-9 mb-2' : '']"
85+
>
86+
<lfx-leaderboard-mobile-nav :leaderboard-key="config.key" />
87+
</div>
88+
<p
89+
:class="[scrollTop < scrollThreshold ? 'block' : 'hidden']"
90+
class="transition-all ease-linear text-sm text-neutral-500 w-full whitespace-pre-wrap min-h-10"
91+
>
92+
{{ config?.description }}
93+
</p>
7594
</div>
7695

77-
<p
78-
:class="[scrollTop < scrollThreshold ? 'block' : 'hidden']"
79-
class="-mt-5 transition-all ease-linear text-sm text-neutral-500 w-full whitespace-pre-wrap min-h-10"
80-
>
81-
{{ config?.description }}
82-
</p>
83-
96+
<lfx-collections-filter
97+
v-model="selectedCollection"
98+
class="flex md:hidden !w-full"
99+
width="100%"
100+
size="medium"
101+
type="filled"
102+
/>
84103
<div class="relative w-full md:block hidden">
85104
<lfx-leaderboard-search
86105
:config="config"
@@ -90,7 +109,7 @@ SPDX-License-Identifier: MIT
90109
</div>
91110
<div class="md:hidden block w-full">
92111
<div
93-
class="rounded-full border border-solid border-neutral-200 cursor-pointer flex items-center gap-2 px-3 py-2"
112+
class="rounded-full bg-neutral-50 border border-solid border-neutral-200 cursor-pointer flex items-center gap-2 px-3 py-2"
94113
@click="isSearchOpen = true"
95114
>
96115
<lfx-icon
@@ -111,7 +130,6 @@ SPDX-License-Identifier: MIT
111130
<div class="p-1 bg-white rounded-lg">
112131
<lfx-leaderboard-search
113132
ref="searchComponentRef"
114-
class="bg-white"
115133
in-modal
116134
:config="config"
117135
@item-click="handleItemClick"
@@ -124,7 +142,7 @@ SPDX-License-Identifier: MIT
124142
import { ref, watch, nextTick } from 'vue';
125143
import pluralize from 'pluralize';
126144
import type { LeaderboardConfig } from '../../config/types/leaderboard.types';
127-
import LfxLeaderboardMobileNav from './leaderboard-mobile-nav.vue';
145+
import LfxCollectionsFilter from '../filters/collections-filter.vue';
128146
import LfxLeaderboardSearch from './leaderboard-search.vue';
129147
import LfxButton from '~/components/uikit/button/button.vue';
130148
import LfxIconButton from '~/components/uikit/icon-button/icon-button.vue';
@@ -134,12 +152,15 @@ import { useShareStore } from '~/components/shared/modules/share/store/share.sto
134152
import { LfxRoutes } from '~/components/shared/types/routes';
135153
import LfxModal from '~/components/uikit/modal/modal.vue';
136154
import type { Leaderboard } from '~~/types/leaderboard/leaderboard';
155+
import LfxLeaderboardMobileNav from '~/components/modules/leaderboards/components/sections/leaderboard-mobile-nav.vue';
137156
138157
const { openShareModal } = useShareStore();
139158
const props = defineProps<{
140159
config: LeaderboardConfig;
141160
}>();
142161
162+
const selectedCollection = defineModel<string>('collectionSlug', { default: '' });
163+
143164
const { scrollTop } = useScroll();
144165
145166
const isSearchOpen = ref(false);

0 commit comments

Comments
 (0)