Skip to content

Commit 64ea865

Browse files
committed
feat: leaderboard collection filter
Signed-off-by: Gašper Grom <[email protected]>
1 parent d7ce3d8 commit 64ea865

File tree

7 files changed

+198
-11
lines changed

7 files changed

+198
-11
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: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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"
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+
<!-- All collections option -->
33+
<lfx-dropdown-item
34+
value="all"
35+
label="All collections"
36+
/>
37+
38+
<lfx-dropdown-separator />
39+
40+
<!-- Search input -->
41+
<lfx-dropdown-search
42+
v-model="searchQuery"
43+
placeholder="Search collections..."
44+
lazy
45+
/>
46+
47+
<lfx-dropdown-separator />
48+
49+
<!-- Collections list -->
50+
<div
51+
v-if="isPending"
52+
class="py-8 flex justify-center"
53+
>
54+
<lfx-spinner />
55+
</div>
56+
57+
<div
58+
v-else-if="!collections.length && searchQuery"
59+
class="py-4 px-3 text-sm text-neutral-500 text-center"
60+
>
61+
No collections found
62+
</div>
63+
64+
<template v-else>
65+
<lfx-dropdown-item
66+
v-for="collection in collections"
67+
:key="collection.id"
68+
:value="collection.slug"
69+
:label="collection.name"
70+
/>
71+
</template>
72+
</template>
73+
</lfx-dropdown-select>
74+
</template>
75+
76+
<script setup lang="ts">
77+
import { computed, ref, watch } from 'vue';
78+
import LfxDropdownSelect from '~/components/uikit/dropdown/dropdown-select.vue';
79+
import LfxDropdownSelector from '~/components/uikit/dropdown/dropdown-selector.vue';
80+
import LfxDropdownItem from '~/components/uikit/dropdown/dropdown-item.vue';
81+
import LfxDropdownSearch from '~/components/uikit/dropdown/dropdown-search.vue';
82+
import LfxDropdownSeparator from '~/components/uikit/dropdown/dropdown-separator.vue';
83+
import LfxIcon from '~/components/uikit/icon/icon.vue';
84+
import type { Collection } from '~~/types/collection';
85+
import type { Pagination } from '~~/types/shared/pagination';
86+
import { COLLECTIONS_API_SERVICE } from '~/components/modules/collection/services/collections.api.service';
87+
import LfxSpinner from '~/components/uikit/spinner/spinner.vue';
88+
89+
const props = withDefaults(
90+
defineProps<{
91+
modelValue?: string;
92+
width?: string;
93+
matchWidth?: boolean;
94+
size?: 'medium' | 'small';
95+
type?: 'transparent' | 'filled';
96+
}>(),
97+
{
98+
modelValue: '',
99+
width: '350px',
100+
matchWidth: false,
101+
size: 'medium',
102+
type: 'transparent',
103+
},
104+
);
105+
106+
const emit = defineEmits<{
107+
(e: 'update:modelValue', value: string): void;
108+
}>();
109+
110+
const selectedCollection = computed({
111+
get: () => props.modelValue,
112+
set: (value: string) => emit('update:modelValue', value),
113+
});
114+
115+
const searchQuery = ref('');
116+
const collections = ref<Collection[]>([]);
117+
const isPending = ref(false);
118+
119+
// Fetch collections from API
120+
const fetchCollections = async () => {
121+
isPending.value = true;
122+
try {
123+
const response = await COLLECTIONS_API_SERVICE.searchCollections(searchQuery.value);
124+
collections.value = (response as Pagination<Collection>).data || [];
125+
} catch (error) {
126+
console.error('Error fetching collections:', error);
127+
collections.value = [];
128+
} finally {
129+
isPending.value = false;
130+
}
131+
};
132+
133+
// Watch for search query changes
134+
watch(
135+
() => searchQuery.value,
136+
() => {
137+
fetchCollections();
138+
},
139+
{ immediate: true },
140+
);
141+
</script>
142+
143+
<script lang="ts">
144+
export default {
145+
name: 'LfxCollectionsFilter',
146+
};
147+
</script>

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,21 @@ SPDX-License-Identifier: MIT
3333
/>
3434
</div>
3535

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>
36+
<div class="gap-1 w-full">
37+
<div class="flex justify-between items-center w-full">
38+
<h1
39+
:class="[scrollTop > scrollThreshold ? 'text-2xl ml-13 -mt-12' : 'text-3xl']"
40+
class="transition-all ease-linear font-light font-secondary text-neutral-900 md:block hidden"
41+
>
42+
{{ config?.name }}
43+
</h1>
44+
<lfx-collections-filter
45+
v-model="selectedCollection"
46+
width="350px"
47+
size="medium"
48+
type="filled"
49+
/>
50+
</div>
4351
<!-- Sidebar navigation -->
4452
<div
4553
class="md:hidden flex justify-start"
@@ -58,7 +66,7 @@ SPDX-License-Identifier: MIT
5866
<div class="mt-1">
5967
<div class="md:block hidden">
6068
<lfx-button
61-
type="tertiary"
69+
type="ghost"
6270
size="small"
6371
button-style="pill"
6472
class="h-9"
@@ -123,6 +131,7 @@ SPDX-License-Identifier: MIT
123131
import { ref, watch, nextTick } from 'vue';
124132
import pluralize from 'pluralize';
125133
import type { LeaderboardConfig } from '../../config/types/leaderboard.types';
134+
import LfxCollectionsFilter from '../filters/collections-filter.vue';
126135
import LfxLeaderboardMobileNav from './leaderboard-mobile-nav.vue';
127136
import LfxLeaderboardSearch from './leaderboard-search.vue';
128137
import LfxButton from '~/components/uikit/button/button.vue';
@@ -139,6 +148,8 @@ const props = defineProps<{
139148
config: LeaderboardConfig;
140149
}>();
141150
151+
const selectedCollection = defineModel<string>('collectionSlug', { default: '' });
152+
142153
const { scrollTop } = useScroll();
143154
144155
const isSearchOpen = ref(false);

frontend/app/components/modules/leaderboards/components/views/leaderboard-detail.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ SPDX-License-Identifier: MIT
2929
<div class="sticky lg:top-17 top-14 z-10 bg-white sm:pt-10 pt-6">
3030
<!-- Header section -->
3131
<lfx-leaderboard-detail-header
32+
v-model:collection-slug="collectionSlug"
3233
:config="leaderboardConfig"
3334
@item-click="handleSearchItemClick"
3435
/>
@@ -78,10 +79,12 @@ const props = defineProps<{
7879
}>();
7980
8081
const isScrollingIntoRow = ref<boolean>(false);
82+
const collectionSlug = ref<string>('');
8183
8284
const params = computed(() => ({
8385
leaderboardType: props.leaderboardKey,
8486
initialPageSize: 100,
87+
collectionSlug: collectionSlug.value && collectionSlug.value !== 'all' ? collectionSlug.value : undefined,
8588
}));
8689
8790
const { data, isPending, isFetchingNextPage, fetchNextPage, hasNextPage } =

frontend/app/components/modules/leaderboards/services/leaderboard.api.service.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,25 @@ export interface LeaderboardDetailQueryParams {
1616
leaderboardType: string;
1717
search?: string;
1818
initialPageSize?: number;
19+
collectionSlug?: string;
1920
}
2021

2122
const DEFAULT_PAGE_SIZE = 100;
2223
class LeaderboardApiService {
2324
async prefetchLeaderboardDetails(params: ComputedRef<LeaderboardDetailQueryParams>) {
2425
const queryClient = useQueryClient();
25-
const queryKey = computed(() => [TanstackKey.LEADERBOARD_DETAIL, params.value.leaderboardType]);
26+
const queryKey = computed(() => [
27+
TanstackKey.LEADERBOARD_DETAIL,
28+
params.value.leaderboardType,
29+
params.value.collectionSlug,
30+
]);
2631

2732
const queryFn = computed<QueryFunction<Pagination<Leaderboard>>>(() =>
2833
this.leaderboardDetailQueryFn(() => ({
2934
leaderboardType: params.value.leaderboardType,
3035
initialPageSize: params.value.initialPageSize,
3136
search: params.value.search,
37+
collectionSlug: params.value.collectionSlug,
3238
})),
3339
);
3440

@@ -59,13 +65,18 @@ class LeaderboardApiService {
5965
}
6066

6167
fetchLeaderboardDetails(params: ComputedRef<LeaderboardDetailQueryParams>) {
62-
const queryKey = computed(() => [TanstackKey.LEADERBOARD_DETAIL, params.value.leaderboardType]);
68+
const queryKey = computed(() => [
69+
TanstackKey.LEADERBOARD_DETAIL,
70+
params.value.leaderboardType,
71+
params.value.collectionSlug,
72+
]);
6373

6474
const queryFn = computed<QueryFunction<Pagination<Leaderboard>>>(() =>
6575
this.leaderboardDetailQueryFn(() => ({
6676
leaderboardType: params.value.leaderboardType,
6777
initialPageSize: params.value.initialPageSize,
6878
search: params.value.search,
79+
collectionSlug: params.value.collectionSlug,
6980
})),
7081
);
7182

@@ -87,7 +98,7 @@ class LeaderboardApiService {
8798
leaderboardDetailQueryFn(
8899
query: () => Record<string, string | number | boolean | undefined | string[] | null>,
89100
): QueryFunction<Pagination<Leaderboard>, readonly unknown[], number> {
90-
const { leaderboardType, initialPageSize, search } = query();
101+
const { leaderboardType, initialPageSize, search, collectionSlug } = query();
91102
return async ({ pageParam = 0 }) => {
92103
const pageSize = pageParam === 0 ? (initialPageSize ?? DEFAULT_PAGE_SIZE) : DEFAULT_PAGE_SIZE;
93104

@@ -96,6 +107,7 @@ class LeaderboardApiService {
96107
page: pageParam,
97108
pageSize,
98109
search,
110+
collectionSlug,
99111
},
100112
});
101113
};

frontend/server/api/collection/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default defineEventHandler(async (event): Promise<Pagination<Collection>
3131
const query = getQuery(event);
3232
const sort: string = (query?.sort as string) || 'name_asc';
3333
const categories: string | undefined = (query?.categories as string) || undefined;
34+
const search: string | undefined = (query?.search as string) || undefined;
3435
const [orderByField, orderByDirection] = sort.split('_');
3536

3637
// Pagination parameters
@@ -43,6 +44,7 @@ export default defineEventHandler(async (event): Promise<Pagination<Collection>
4344
count,
4445
page,
4546
pageSize,
47+
search,
4648
categoryIds: categories?.length ? categories : undefined,
4749
orderByField,
4850
orderByDirection,

frontend/server/api/leaderboard/[type].ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default defineEventHandler(async (event): Promise<Pagination<Leaderboard>
1111
const page: number = (query.page as number) || 0;
1212
const pageSize: number = (query.pageSize as number) || 20;
1313
const search: string | undefined = (query.search as string) || undefined;
14+
const collectionSlug: string | undefined = (query.collectionSlug as string) || undefined;
1415

1516
if (!type) {
1617
throw createError({
@@ -25,6 +26,7 @@ export default defineEventHandler(async (event): Promise<Pagination<Leaderboard>
2526
page,
2627
pageSize,
2728
search,
29+
collectionSlug,
2830
});
2931

3032
return {

0 commit comments

Comments
 (0)