Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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