Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
adeb282
chore: partial work on leaderboard configs
emlimlf Nov 4, 2025
7176c7c
feat: wired leaderboard detail page to api
emlimlf Nov 4, 2025
dd2c137
chore: fix results scrolling
emlimlf Nov 4, 2025
ae08287
feat: wired search input box to api
emlimlf Nov 4, 2025
ecfbbe2
chore: fix all the icons and text on the config
emlimlf Nov 4, 2025
5f53579
chore: added leaderboard landing route
emlimlf Nov 4, 2025
da03cbf
chore: partial work on leaderboard configs
emlimlf Nov 4, 2025
69f0f4e
feat: wired leaderboard detail page to api
emlimlf Nov 4, 2025
2c44f55
chore: fix results scrolling
emlimlf Nov 4, 2025
6c15737
feat: wired search input box to api
emlimlf Nov 4, 2025
0d2c8bf
chore: fix all the icons and text on the config
emlimlf Nov 4, 2025
0cc627f
chore: added leaderboard landing route
emlimlf Nov 4, 2025
b507fc7
Merge branch 'feat/insights-leaderboard' of github.com:linuxfoundatio…
emlimlf Nov 4, 2025
5f398e0
chore: partial work on leaderboard landing page
emlimlf Nov 4, 2025
797fb87
feat: wired leaderboard landing to api
emlimlf Nov 5, 2025
5f31598
chore: added table header to leaderboard card
emlimlf Nov 5, 2025
c1bfc6e
feat: implement share button
emlimlf Nov 5, 2025
8e192b0
chore: make details page responsive
emlimlf Nov 5, 2025
2159bee
Merge branch 'main' into feat/insights-leaderboard
emlimlf Nov 5, 2025
a3caa1f
fix: search input not clickable
emlimlf Nov 5, 2025
4cb48e1
chore: add leaderboard to ssr cache
emlimlf Nov 5, 2025
b1b32dc
chore: fix missing bracket
emlimlf Nov 5, 2025
70bfd8c
chore: added 4 more configs
emlimlf Nov 5, 2025
e7be9fc
chore: enabled codebase size and fix table overflow
emlimlf Nov 5, 2025
148b9d8
chore: added fastest mergers config and fix trend display
emlimlf Nov 5, 2025
abb1a58
chore: hide leaderboard from menu
emlimlf Nov 5, 2025
db79382
chore: enabled resolution rate and handle float values
emlimlf Nov 6, 2025
a22632c
chore: added trend display tooltip
emlimlf Nov 6, 2025
2814b17
chore: fix mobile responsive for detail page
emlimlf Nov 6, 2025
57c97ab
chore: added clear search text
emlimlf Nov 6, 2025
4b0738b
chore: changed pagination size and fix share title
emlimlf Nov 6, 2025
1d6df49
feat: implement scroll to row on searched item
emlimlf Nov 6, 2025
9c4b15b
fix: address qa items
emlimlf Nov 6, 2025
01ebede
chore: implement virtual list
emlimlf Nov 6, 2025
bd30a0a
chore: minor style adjustments and disable longest running
emlimlf Nov 6, 2025
e788826
fix: smoother detail header animation
emlimlf Nov 6, 2025
91e69c0
fix: mobile issues and column label changes
emlimlf Nov 6, 2025
f9ff739
fix: table padding top
emlimlf Nov 6, 2025
489ea6d
chore: enable leaderboard links on header and footer
emlimlf Nov 7, 2025
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
128 changes: 128 additions & 0 deletions frontend/app/components/modules/leaderboards/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Leaderboards Component

This directory contains the leaderboard components for displaying various project rankings and metrics.

## Components

### LeaderboardDetail (`components/views/leaderboard-detail.vue`)

A comprehensive leaderboard view component that displays a ranked list of projects with their metrics and trends.

#### Features

- **Three-column layout**: Back button, main content (720px), and sidebar navigation (260px)
- **Search functionality**: Filter projects by name
- **Trend indicators**: Show up/down/neutral trends with percentages and change values
- **Responsive design**: Fixed widths for consistent layout
- **Dynamic routing**: Navigate between different leaderboards
- **Share functionality**: Share leaderboard via native share API or clipboard

#### Props

- `leaderboardKey` (required): String - The key identifying which leaderboard to display
- `items`: Array - List of leaderboard items (default: [])
- `highlightedIndex`: Number - Index of item to highlight (default: -1)

#### LeaderboardItem Interface

```typescript
interface LeaderboardItem {
id: string;
name: string;
logo?: string;
value: number;
trend?: LeaderboardTrend;
}

interface LeaderboardTrend {
direction: 'up' | 'down' | 'neutral';
percentage: number;
change: number;
}
```

#### Usage Example

```vue
<template>
<lfx-leaderboard-detail
leaderboard-key="most-active-contributors"
:items="leaderboardItems"
:highlighted-index="1"
/>
</template>

<script setup lang="ts">
import LfxLeaderboardDetail from '~/components/modules/leaderboards/components/views/leaderboard-detail.vue';

const leaderboardItems = [
{
id: '1',
name: 'The Linux Kernel',
logo: 'https://example.com/logo.png',
value: 100,
trend: {
direction: 'up',
percentage: 2.3,
change: 120,
},
},
// ... more items
];
</script>
```

## Configuration

### Leaderboard Configs (`config/`)

Each leaderboard has a configuration file that defines:

- `key`: Unique identifier
- `name`: Display name
- `icon`: FontAwesome icon name
- `dataDisplay`: Component for rendering the metric
- `sort`: Sort order
- `columnLabel`: Label for the metric column

#### Example Config

```typescript
export const mostActiveContributorsConfig: LeaderboardConfig = {
key: 'most-active-contributors',
name: 'Most Active Contributors',
icon: 'people-group',
dataDisplay: NumericDataDisplay,
sort: 'mostActiveContributors_DESC',
columnLabel: 'Contributors (12M)',
};
```

## Data Display Components

Located in `components/data-displays/`, these components format the metric values:

- `numeric.vue`: Formats numbers with proper thousand separators
- `time-duration.vue`: Formats time durations

## Routing

The leaderboard detail view is accessible via:

```
/leaderboards/[key]
```

Where `[key]` is the leaderboard configuration key (e.g., `most-active-contributors`).

## Styling

The component uses Tailwind CSS with the project's design system:

- Colors: `neutral-*`, `positive-600`, `negative-600`, `brand-500`
- Fonts: Primary font for body text, `font-secondary` (Roboto Slab) for headings
- Spacing: Consistent with project standards (gap-2, gap-3, etc.)

## Navigation

The sidebar navigation displays all available leaderboards and highlights the active one. Clicking on a leaderboard navigates to its detail page.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!--
Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->

<template>
<span>{{ formattedNumeric }}</span>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { formatNumber } from '~/components/shared/utils/formatter';

const props = withDefaults(
defineProps<{
value: number;
}>(),
{
value: 0,
},
);

/**
* Formats a number to a numeric string with up to 2 units
* Example: 1,000,000
*/
const formattedNumeric = computed(() => {
if (!props.value || props.value === 0) {
return '0';
}

return formatNumber(props.value);
});
</script>

<script lang="ts">
export default {
name: 'NumericDataDisplay',
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!--
Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->

<template>
<lfx-tooltip
:content="dateFormatted"
class="!w-full flex justify-end"
placement="top-end"
>
<span v-if="value">{{ formattedDuration }}</span>
<span v-else>-</span>
</lfx-tooltip>
</template>

<script setup lang="ts">
import { DateTime } from 'luxon';
import { computed } from 'vue';
import LfxTooltip from '~/components/uikit/tooltip/tooltip.vue';

const props = withDefaults(
defineProps<{
value: number; // Time in milliseconds
}>(),
{
value: 0,
},
);

/**
* Formats milliseconds to a duration string with up to 2 units
* Example: 2y 3mo, 5d 3h, 1h 30m
*/
const formattedDuration = computed(() => {
if (!props.value || props.value === 0) {
return '0s';
}

// Calculate duration from timestamp to now
const timestamp = DateTime.fromMillis(props.value);
const now = DateTime.now();
const duration = now.diff(timestamp, ['years', 'months', 'days', 'hours', 'minutes', 'seconds']);
const { years, months, days, hours, minutes, seconds } = duration.toObject();

const units: Array<{ value: number; label: string }> = [];

// Build array of non-zero units
if (years && years > 0) {
units.push({ value: Math.floor(years), label: 'y' });
}
if (months && months > 0) {
units.push({ value: Math.floor(months), label: 'mo' });
}

if (days && days > 0) {
units.push({ value: Math.floor(days), label: 'd' });
}

if (hours && hours > 0) {
units.push({ value: Math.floor(hours), label: 'h' });
}
if (minutes && minutes > 0) {
units.push({ value: Math.floor(minutes), label: 'm' });
}
if (seconds && seconds > 0) {
units.push({ value: Math.floor(seconds), label: 's' });
}

// Return up to 2 units
return units
.slice(0, 2)
.map((unit) => `${unit.value}${unit.label}`)
.join(' ');
});

const dateFormatted = computed(() => {
return DateTime.fromMillis(props.value).toFormat('MMM dd, yyyy');
});
</script>

<script lang="ts">
export default {
name: 'TimeDurationDataDisplay',
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!--
Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<div class="flex gap-1 items-center">
<lfx-icon
:name="getTrendIcon"
type="solid"
:size="12"
:class="getTrendColor"
/>
<span
class="text-xs leading-[15px] font-medium"
:class="getTrendColor"
>
{{ trendPercentage }}% ({{ formatTrendValue }})
</span>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import type { Leaderboard } from '~~/types/leaderboard/leaderboard';
import { formatNumber } from '~/components/shared/utils/formatter';

const props = defineProps<{
data: Leaderboard;
isReverse?: boolean;
}>();

const trend = computed(() => {
return props.data.value - props.data.previousPeriodValue;
});

const trendPercentage = computed(() => {
if (!props.data.previousPeriodValue) {
// For division by zero, show as 100% increase
return '100.0';
}
const value = (trend.value / props.data.previousPeriodValue) * 100;
return value.toFixed(1);
});

const formatTrendValue = computed(() => {
const sign = trend.value >= 0 ? '+' : '-';
return `${sign}${formatNumber(Math.abs(trend.value))}`;
});

const trendDirection = computed(() => {
if (trend.value === 0) return 'neutral';

return trend.value > 0 ? 'up' : 'down';
});

const getTrendIcon = computed(() => {
if (trendDirection.value === 'neutral') return 'equals';

const isUp = trendDirection.value === 'up';
const shouldShowUp = props.isReverse ? !isUp : isUp;

return shouldShowUp ? 'circle-arrow-up' : 'circle-arrow-down';
});

const getTrendColor = computed(() => {
if (trendDirection.value === 'neutral') return 'text-brand-500';

const isUp = trendDirection.value === 'up';
const shouldBePositive = props.isReverse ? !isUp : isUp;

return shouldBePositive ? 'text-positive-600' : 'text-negative-600';
});
</script>

<script lang="ts">
export default {
name: 'LeaderboardTrendDisplay',
};
</script>
Loading
Loading