Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,98 @@ Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<div class="bg-white">
<div class="bg-white border-b border-neutral-200">
<div class="container">
<div class="flex items-center gap-3 overflow-x-auto py-5">
<lfx-menu-button
v-for="link of lfCollectionAggregateLinks"
:key="link.key"
:to="{ name: link.routeName, params: { slug: props.slug } }"
:exact="true"
>
<template #default="{ isActive }">
<lfx-icon
:name="link.icon"
:type="isActive ? 'solid' : 'light'"
/>
{{ link.label }}
</template>
</lfx-menu-button>
<div class="flex items-center justify-between gap-3 py-5">
<!-- Desktop: full tab row. Matches project-menu.vue's lg: breakpoint (1024px) so
collection and project pages collapse to the dropdown fallback at the same width. -->
<div class="lg:flex hidden items-center gap-3">
<lfx-menu-button
v-for="link of lfCollectionAggregateLinks"
:key="link.key"
:to="linkTo(link)"
:exact="true"
>
<template #default="{ isActive }">
<lfx-icon
:name="link.icon"
:type="isActive ? 'solid' : 'light'"
/>
{{ link.label }}
</template>
</lfx-menu-button>
</div>
<!-- Tablet/mobile: dropdown fallback, same pattern as project-menu.vue -->
<div class="lg:hidden block min-w-0">
<lfx-dropdown
placement="bottom-start"
width="15rem"
>
<template #trigger>
<lfx-dropdown-selector>
<lfx-icon
:name="activeLink?.icon || ''"
:size="16"
class="text-brand-500 font-black"
/>
{{ activeLink?.label }}
</lfx-dropdown-selector>
</template>

<router-link
v-for="link of lfCollectionAggregateLinks"
:key="link.key"
:to="linkTo(link)"
>
<lfx-dropdown-item
:value="link.key"
:label="link.label"
/>
</router-link>
</lfx-dropdown>
</div>
<lfx-project-date-range-picker
v-if="showDateRangePicker"
class="shrink-0"
/>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'nuxt/app';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import LfxMenuButton from '~/components/uikit/menu-button/menu-button.vue';
import LfxDropdown from '~/components/uikit/dropdown/dropdown.vue';
import LfxDropdownSelector from '~/components/uikit/dropdown/dropdown-selector.vue';
import LfxDropdownItem from '~/components/uikit/dropdown/dropdown-item.vue';
import LfxProjectDateRangePicker from '~/components/modules/project/components/shared/header/date-range-picker.vue';
import { lfCollectionAggregateLinks } from '~/components/modules/collection/config/collection-links';

const props = defineProps<{
slug: string;
showDateRangePicker?: boolean;
}>();

const route = useRoute();
const activeLink = computed(
() => lfCollectionAggregateLinks.find((link) => link.routeName === route.name) || lfCollectionAggregateLinks[0],
);

// Preserve the current query (date range: timeRange/start/end, plus onlyLFProjects) when
// switching tabs, so the selected date range doesn't reset to the default on every tab change.
// `widget` is dropped because it's a per-tab scroll target, not shared across tabs - mirrors
// project-menu.vue's linkUrl().
const linkTo = (link: (typeof lfCollectionAggregateLinks)[number]) => ({
name: link.routeName,
params: { slug: props.slug },
query: {
...route.query,
widget: undefined,
},
});
</script>

<script lang="ts">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ SPDX-License-Identifier: MIT
<template v-if="loading">
<lfx-skeleton
v-for="i in 3"
:key="i"
:key="`skeleton-${i}`"
height="2rem"
width="9rem"
class="rounded-full"
/>
</template>
<template v-else>
<lfx-chip
key="projects-repositories"
type="bordered"
size="small"
class="flex items-center gap-1"
Expand All @@ -32,6 +33,7 @@ SPDX-License-Identifier: MIT
</lfx-chip>

<lfx-chip
key="contributors"
type="bordered"
size="small"
class="flex items-center gap-1"
Expand All @@ -48,6 +50,7 @@ SPDX-License-Identifier: MIT
</lfx-chip>

<lfx-chip
key="avg-health"
type="bordered"
size="small"
class="flex items-center gap-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ SPDX-License-Identifier: MIT
class="hidden md:flex transition-all ease-linear items-center gap-4 lg:w-auto shrink-0 mt-4 lg:mt-0"
>
<lfx-toggle
v-if="scrollTop > 50"
v-if="scrollTop > 50 && isProjectsTab"
v-model="isOnlyLFProjects"
>
Only Linux Foundation projects
Expand Down Expand Up @@ -356,7 +356,7 @@ SPDX-License-Identifier: MIT
:loading="props.metricsLoading"
/>
<lfx-toggle
v-if="scrollTop <= 50"
v-if="scrollTop <= 50 && isProjectsTab"
v-model="isOnlyLFProjects"
class="!hidden md:!flex"
>
Expand Down Expand Up @@ -385,7 +385,7 @@ SPDX-License-Identifier: MIT
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useRouter } from 'nuxt/app';
import { useRoute, useRouter } from 'nuxt/app';
import { useQueryClient } from '@tanstack/vue-query';
import { collectionTabs, headerBackground, CollectionTypeEnum } from '../../config/collection-type-config';
import LfxCollectionMetricsRow from './collection-metrics-row.vue';
Expand Down Expand Up @@ -425,8 +425,14 @@ const authStore = useAuthStore();
const { user } = storeToRefs(authStore);

const router = useRouter();
const route = useRoute();
const { openShareModal } = useShareStore();

// The LF-projects toggle filters the Projects tab's table - it has no effect on the
// Contributors/Popularity/Development tabs, which show collection-wide aggregate widgets
// instead of a filterable project list, so it's hidden there.
const isProjectsTab = computed(() => route.name === LfxRoutes.COLLECTION);

const props = defineProps<{
collection?: Collection;
loading?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ export interface CollectionLinkConfig {

export const lfCollectionAggregateLinks: CollectionLinkConfig[] = [
{ key: 'projects', icon: 'layer-group', label: 'Projects', routeName: LfxRoutes.COLLECTION },
// {
// key: 'contributors',
// icon: 'people-group',
// label: 'Contributors',
// routeName: LfxRoutes.COLLECTION_CONTRIBUTORS,
// },
// {
// key: 'popularity',
// icon: 'fire',
// label: 'Popularity',
// routeName: LfxRoutes.COLLECTION_POPULARITY,
// },
// {
// key: 'development',
// icon: 'code',
// label: 'Development',
// routeName: LfxRoutes.COLLECTION_DEVELOPMENT,
// },
{
key: 'contributors',
icon: 'people-group',
label: 'Contributors',
routeName: LfxRoutes.COLLECTION_CONTRIBUTORS,
},
{
key: 'popularity',
icon: 'fire',
label: 'Popularity',
routeName: LfxRoutes.COLLECTION_POPULARITY,
},
{
key: 'development',
icon: 'code',
label: 'Development',
routeName: LfxRoutes.COLLECTION_DEVELOPMENT,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@ import {
import { type ComputedRef, type Ref, computed } from 'vue';
import { isArray } from 'lodash-es';
import type { Pagination } from '~~/types/shared/pagination';
import type {
Collection,
CollectionMetrics,
CollectionType,
CollectionContributorLeaderboardItem,
CollectionPopularityAggregate,
CollectionDevelopmentAggregate,
} from '~~/types/collection';
import type { Collection, CollectionMetrics, CollectionType } from '~~/types/collection';
import type { Category, CategoryGroup } from '~~/types/category';
import type { ProjectInsights } from '~~/types/project';
import { TanstackKey } from '~/components/shared/types/tanstack';
Expand Down Expand Up @@ -585,30 +578,6 @@ class CollectionsApiService {
});
}

fetchCollectionContributors(slug: string, fetchFn: typeof $fetch = $fetch) {
return useQuery<CollectionContributorLeaderboardItem[]>({
queryKey: [TanstackKey.COLLECTION_CONTRIBUTORS, slug],
queryFn: () => fetchFn(`/api/collection/${slug}/contributors`),
enabled: !!slug,
});
}

fetchCollectionPopularity(slug: string, fetchFn: typeof $fetch = $fetch) {
return useQuery<CollectionPopularityAggregate>({
queryKey: [TanstackKey.COLLECTION_POPULARITY, slug],
queryFn: () => fetchFn(`/api/collection/${slug}/popularity`),
enabled: !!slug,
});
}

fetchCollectionDevelopment(slug: string, fetchFn: typeof $fetch = $fetch) {
return useQuery<CollectionDevelopmentAggregate>({
queryKey: [TanstackKey.COLLECTION_DEVELOPMENT, slug],
queryFn: () => fetchFn(`/api/collection/${slug}/development`),
enabled: !!slug,
});
}

isEmptyData(value: Collection[] | null | undefined) {
// check if the value is null or undefined or the length of the value is 0
if (value === null || value === undefined || value?.length === 0 || !isArray(value)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,12 @@ Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<div class="container py-5 lg:py-10">
<h1 class="text-heading-3 font-secondary font-bold mb-5">Contributors</h1>

<lfx-project-load-state
:status="status"
:error="error"
error-message="Error fetching collection contributors"
:is-empty="isEmpty"
>
<lfx-table>
<thead>
<tr>
<th class="text-left text-xs font-semibold text-neutral-500 py-3 px-2">Contributor</th>
<th class="text-left text-xs font-semibold text-neutral-500 py-3 px-2">Total contributions</th>
</tr>
</thead>
<tbody>
<tr
v-for="contributor in contributors"
:key="contributor.id"
class="border-t border-neutral-100"
>
<td class="py-3 px-2">
<div class="flex items-center gap-2">
<lfx-avatar
:src="contributor.avatar"
type="member"
:aria-label="contributor.displayName"
/>
<lfx-tooltip :disabled="!contributor.githubHandleArray?.length">
<template
v-if="contributor.githubHandleArray?.length"
#content
>
<div class="flex flex-col gap-2">
<a
v-for="githubHandle in contributor.githubHandleArray"
:key="githubHandle"
:href="`https://github.com/${githubHandle}`"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-1 text-white"
>
<lfx-icon
name="github"
type="brands"
:size="14"
/>
<span class="text-xs font-semibold">{{ githubHandle }}</span>
</a>
</div>
</template>
<span>{{ contributor.displayName }}</span>
</lfx-tooltip>
</div>
</td>
<td class="py-3 px-2">{{ formatNumber(contributor.contributionCount) }}</td>
</tr>
</tbody>
</lfx-table>
</lfx-project-load-state>
</div>
<lfx-widget-area :name="WidgetArea.CONTRIBUTORS" />
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useRequestFetch } from 'nuxt/app';
import LfxAvatar from '~/components/uikit/avatar/avatar.vue';
import LfxTable from '~/components/uikit/table/table.vue';
import LfxTooltip from '~/components/uikit/tooltip/tooltip.vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import LfxProjectLoadState from '~/components/modules/project/components/shared/load-state.vue';
import { formatNumber } from '~/components/shared/utils/formatter';
import { COLLECTIONS_API_SERVICE } from '~/components/modules/collection/services/collections.api.service';

const props = defineProps<{
slug: string;
}>();

const requestFetch = useRequestFetch();

const { data, status, error } = COLLECTIONS_API_SERVICE.fetchCollectionContributors(props.slug, requestFetch);

const contributors = computed(() => data.value || []);
const isEmpty = computed(() => contributors.value.length === 0);
import LfxWidgetArea from '~/components/modules/widget/components/shared/widget-area.vue';
import { WidgetArea } from '~/components/modules/widget/types/widget-area';
</script>

<script lang="ts">
Expand Down
Loading
Loading