diff --git a/backend/apps/core/utils/index.py b/backend/apps/core/utils/index.py
index 7e7d799ae6..40637c91f4 100644
--- a/backend/apps/core/utils/index.py
+++ b/backend/apps/core/utils/index.py
@@ -196,6 +196,7 @@ def get_params_for_index(index_name: str) -> dict:
case "users":
params["attributesToRetrieve"] = [
"idx_avatar_url",
+ "idx_badge_count",
"idx_bio",
"idx_company",
"idx_created_at",
diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py
index 1d99857d97..f9fbb7198e 100644
--- a/backend/apps/github/api/internal/nodes/user.py
+++ b/backend/apps/github/api/internal/nodes/user.py
@@ -28,18 +28,27 @@
class UserNode:
"""GitHub user node."""
+ @strawberry.field
+ def badge_count(self) -> int:
+ """Resolve badge count."""
+ return self.user_badges.filter(is_active=True).count()
+
@strawberry.field
def badges(self) -> list[BadgeNode]:
"""Return user badges."""
user_badges = (
- self.user_badges.select_related("badge")
- .filter(is_active=True)
+ self.user_badges.filter(
+ is_active=True,
+ )
+ .select_related(
+ "badge",
+ )
.order_by(
"badge__weight",
"badge__name",
)
)
- return [ub.badge for ub in user_badges]
+ return [user_badge.badge for user_badge in user_badges]
@strawberry.field
def created_at(self) -> float:
diff --git a/backend/apps/github/index/registry/user.py b/backend/apps/github/index/registry/user.py
index e66b1a14c1..e2a2c85dca 100644
--- a/backend/apps/github/index/registry/user.py
+++ b/backend/apps/github/index/registry/user.py
@@ -14,6 +14,7 @@ class UserIndex(IndexBase):
fields = (
"idx_avatar_url",
+ "idx_badge_count",
"idx_bio",
"idx_company",
"idx_contributions",
diff --git a/backend/apps/github/index/search/user.py b/backend/apps/github/index/search/user.py
index c85e4b6543..4cc99ec11e 100644
--- a/backend/apps/github/index/search/user.py
+++ b/backend/apps/github/index/search/user.py
@@ -32,6 +32,7 @@ def get_users(
"attributesToRetrieve": attributes
or [
"idx_avatar_url",
+ "idx_badge_count",
"idx_bio",
"idx_company",
"idx_contributions",
diff --git a/backend/apps/github/models/mixins/user.py b/backend/apps/github/models/mixins/user.py
index b7b09c821d..bdbb2c2f19 100644
--- a/backend/apps/github/models/mixins/user.py
+++ b/backend/apps/github/models/mixins/user.py
@@ -24,6 +24,11 @@ def idx_avatar_url(self) -> str:
"""Return avatar URL for indexing."""
return self.avatar_url
+ @property
+ def idx_badge_count(self) -> int:
+ """Return badge count for indexing."""
+ return self.user_badges.filter(is_active=True).count()
+
@property
def idx_bio(self) -> str:
"""Return bio for indexing."""
diff --git a/backend/apps/nest/migrations/0007_alter_badge_css_class.py b/backend/apps/nest/migrations/0007_alter_badge_css_class.py
new file mode 100644
index 0000000000..fdb7ad07d7
--- /dev/null
+++ b/backend/apps/nest/migrations/0007_alter_badge_css_class.py
@@ -0,0 +1,29 @@
+# Generated by Django 5.2.6 on 2025-10-09 04:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("nest", "0006_delete_apikey"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="badge",
+ name="css_class",
+ field=models.CharField(
+ choices=[
+ ("award", "Award"),
+ ("medal", "Medal"),
+ ("ribbon", "Ribbon"),
+ ("star", "Star"),
+ ("certificate", "Certificate"),
+ ("bug_slash", "Bug Slash"),
+ ],
+ default="medal",
+ max_length=255,
+ verbose_name="CSS Class",
+ ),
+ ),
+ ]
diff --git a/backend/apps/nest/models/badge.py b/backend/apps/nest/models/badge.py
index 1378dda041..112b3d1575 100644
--- a/backend/apps/nest/models/badge.py
+++ b/backend/apps/nest/models/badge.py
@@ -10,6 +10,14 @@
class Badge(BulkSaveModel, TimestampedModel):
"""Represents a user badge for roles or achievements."""
+ class BadgeCssClass(models.TextChoices):
+ AWARD = "award", "Award"
+ BUG_SLASH = "bug_slash", "Bug Slash"
+ CERTIFICATE = "certificate", "Certificate"
+ MEDAL = "medal", "Medal"
+ RIBBON = "ribbon", "Ribbon"
+ STAR = "star", "Star"
+
class Meta:
db_table = "nest_badges"
ordering = ["weight", "name"]
@@ -18,7 +26,8 @@ class Meta:
css_class = models.CharField(
verbose_name="CSS Class",
max_length=255,
- default="",
+ choices=BadgeCssClass.choices,
+ default=BadgeCssClass.MEDAL,
)
description = models.CharField(
verbose_name="Description",
diff --git a/backend/tests/apps/core/utils/match_test.py b/backend/tests/apps/core/utils/match_test.py
index 39b1a667e7..b8c2efb751 100644
--- a/backend/tests/apps/core/utils/match_test.py
+++ b/backend/tests/apps/core/utils/match_test.py
@@ -114,6 +114,7 @@ def test_get_params_for_users(self):
"typoTolerance": "min",
"attributesToRetrieve": [
"idx_avatar_url",
+ "idx_badge_count",
"idx_bio",
"idx_company",
"idx_created_at",
diff --git a/backend/tests/apps/github/api/internal/nodes/user_test.py b/backend/tests/apps/github/api/internal/nodes/user_test.py
index f83e831433..8da4ba1943 100644
--- a/backend/tests/apps/github/api/internal/nodes/user_test.py
+++ b/backend/tests/apps/github/api/internal/nodes/user_test.py
@@ -5,6 +5,7 @@
import pytest
from apps.github.api.internal.nodes.user import UserNode
+from apps.nest.api.internal.nodes.badge import BadgeNode
class TestUserNode:
@@ -19,6 +20,7 @@ def test_meta_configuration(self):
field_names = {field.name for field in UserNode.__strawberry_definition__.fields}
expected_field_names = {
"avatar_url",
+ "badge_count",
"badges",
"bio",
"company",
@@ -81,42 +83,110 @@ def test_url_field(self):
result = UserNode.url(mock_user)
assert result == "https://github.com/testuser"
- def test_badges_resolver_behavior(self):
- """Unit test verifies the badges method returns badges sorted by weight."""
- badge_high = Mock()
- badge_high.weight = 1
- badge_high.name = "High Priority Badge"
-
- badge_medium = Mock()
- badge_medium.weight = 5
- badge_medium.name = "Medium Priority Badge"
+ def test_badge_count_field(self):
+ """Test badge_count field resolution."""
+ mock_user = Mock()
+ mock_badges_queryset = Mock()
+ mock_badges_queryset.filter.return_value.count.return_value = 3
+ mock_user.user_badges = mock_badges_queryset
- badge_low = Mock()
- badge_low.weight = 10
- badge_low.name = "Low Priority Badge"
+ result = UserNode.badge_count(mock_user)
+ assert result == 3
+ mock_badges_queryset.filter.assert_called_once_with(is_active=True)
+ mock_badges_queryset.filter.return_value.count.assert_called_once()
- user_badge_high = Mock()
- user_badge_high.badge = badge_high
+ def test_badges_field_empty(self):
+ """Test badges field resolution with no badges."""
+ mock_user = Mock()
+ mock_badges_queryset = Mock()
+ mock_filter = mock_badges_queryset.filter.return_value
+ mock_select_related = mock_filter.select_related.return_value
+ mock_select_related.order_by.return_value = []
+ mock_user.user_badges = mock_badges_queryset
- user_badge_medium = Mock()
- user_badge_medium.badge = badge_medium
+ result = UserNode.badges(mock_user)
+ assert result == []
+ mock_badges_queryset.filter.assert_called_once_with(is_active=True)
+ mock_filter.select_related.assert_called_once_with("badge")
+ mock_select_related.order_by.assert_called_once_with("badge__weight", "badge__name")
- user_badge_low = Mock()
- user_badge_low.badge = badge_low
+ def test_badges_field_single_badge(self):
+ """Test badges field resolution with single badge."""
+ mock_user = Mock()
+ mock_badge = Mock(spec=BadgeNode)
+ mock_user_badge = Mock()
+ mock_user_badge.badge = mock_badge
- sorted_user_badges = [user_badge_high, user_badge_medium, user_badge_low]
+ mock_badges_queryset = Mock()
+ mock_filter = mock_badges_queryset.filter.return_value
+ mock_select_related = mock_filter.select_related.return_value
+ mock_select_related.order_by.return_value = [mock_user_badge]
+ mock_user.user_badges = mock_badges_queryset
+ result = UserNode.badges(mock_user)
+ assert result == [mock_badge]
+ mock_badges_queryset.filter.assert_called_once_with(is_active=True)
+ mock_filter.select_related.assert_called_once_with("badge")
+ mock_select_related.order_by.assert_called_once_with("badge__weight", "badge__name")
+
+ def test_badges_field_sorted_by_weight_and_name(self):
+ """Test badges field resolution with multiple badges sorted by weight and name."""
+ # Create mock badges with different weights and names
+ mock_badge_high_weight = Mock(spec=BadgeNode)
+ mock_badge_high_weight.weight = 100
+ mock_badge_high_weight.name = "High Weight Badge"
+
+ mock_badge_medium_weight_a = Mock(spec=BadgeNode)
+ mock_badge_medium_weight_a.weight = 50
+ mock_badge_medium_weight_a.name = "Medium Weight A"
+
+ mock_badge_medium_weight_b = Mock(spec=BadgeNode)
+ mock_badge_medium_weight_b.weight = 50
+ mock_badge_medium_weight_b.name = "Medium Weight B"
+
+ mock_badge_low_weight = Mock(spec=BadgeNode)
+ mock_badge_low_weight.weight = 10
+ mock_badge_low_weight.name = "Low Weight Badge"
+
+ # Create mock user badges
+ mock_user_badge_high = Mock()
+ mock_user_badge_high.badge = mock_badge_high_weight
+
+ mock_user_badge_medium_a = Mock()
+ mock_user_badge_medium_a.badge = mock_badge_medium_weight_a
+
+ mock_user_badge_medium_b = Mock()
+ mock_user_badge_medium_b.badge = mock_badge_medium_weight_b
+
+ mock_user_badge_low = Mock()
+ mock_user_badge_low.badge = mock_badge_low_weight
+
+ # Set up the mock queryset to return badges in the expected sorted order
+ # (lowest weight first, then by name for same weight)
+ mock_badges_queryset = Mock()
+ mock_filter = mock_badges_queryset.filter.return_value
+ mock_select_related = mock_filter.select_related.return_value
+ mock_select_related.order_by.return_value = [
+ mock_user_badge_low, # weight 10
+ mock_user_badge_medium_a, # weight 50, name "Medium Weight A"
+ mock_user_badge_medium_b, # weight 50, name "Medium Weight B"
+ mock_user_badge_high, # weight 100
+ ]
mock_user = Mock()
- mock_queryset = Mock()
- mock_queryset.filter.return_value.order_by.return_value = sorted_user_badges
- mock_user.user_badges.select_related.return_value = mock_queryset
+ mock_user.user_badges = mock_badges_queryset
result = UserNode.badges(mock_user)
- mock_user.user_badges.select_related.assert_called_once_with("badge")
- mock_queryset.filter.assert_called_once_with(is_active=True)
- mock_queryset.filter.return_value.order_by.assert_called_once_with(
- "badge__weight", "badge__name"
- )
-
- assert result == [ub.badge for ub in sorted_user_badges]
+ # Verify the badges are returned in the correct order
+ expected_badges = [
+ mock_badge_low_weight,
+ mock_badge_medium_weight_a,
+ mock_badge_medium_weight_b,
+ mock_badge_high_weight,
+ ]
+ assert result == expected_badges
+
+ # Verify the queryset was called with correct ordering
+ mock_badges_queryset.filter.assert_called_once_with(is_active=True)
+ mock_filter.select_related.assert_called_once_with("badge")
+ mock_select_related.order_by.assert_called_once_with("badge__weight", "badge__name")
diff --git a/frontend/__tests__/unit/components/Badges.test.tsx b/frontend/__tests__/unit/components/Badges.test.tsx
new file mode 100644
index 0000000000..bcec77c5d0
--- /dev/null
+++ b/frontend/__tests__/unit/components/Badges.test.tsx
@@ -0,0 +1,121 @@
+import { render, screen } from '@testing-library/react'
+import React from 'react'
+import Badges from 'components/Badges'
+
+jest.mock('wrappers/FontAwesomeIconWrapper', () => {
+ const RealWrapper = jest.requireActual('wrappers/FontAwesomeIconWrapper').default
+
+ const getName = (icon) => {
+ if (!icon) return 'medal'
+ if (typeof icon === 'string') {
+ const m = icon.match(/fa-([a-z0-9-]+)$/i)
+ if (m) return m[1]
+ const last = icon.trim().split(/\s+/).pop() || ''
+ return last.replace(/^fa-/, '') || 'medal'
+ }
+ if (Array.isArray(icon) && icon.length >= 2) return String(icon[1])
+ if (icon && typeof icon === 'object') return icon.iconName || String(icon[1] ?? 'medal')
+ return 'medal'
+ }
+
+ return function MockFontAwesomeIconWrapper(props) {
+ const name = getName(props.icon)
+ return (
+
+
+
+ )
+ }
+})
+
+jest.mock('@heroui/tooltip', () => ({
+ Tooltip: ({
+ children,
+ content,
+ isDisabled,
+ }: {
+ children: React.ReactNode
+ content: string
+ isDisabled?: boolean
+ }) => {
+ if (isDisabled) {
+ return <>{children}>
+ }
+ return (
+
+ {children}
+
+ )
+ },
+}))
+
+//only for confirming the badges are working properly
+describe('Badges Component', () => {
+ const defaultProps = {
+ name: 'Test Badge',
+ cssClass: 'medal',
+ }
+
+ it('renders valid icon with tooltip', () => {
+ render()
+
+ const icon = screen.getByTestId('badge-icon')
+ expect(icon).toBeInTheDocument()
+ expect(icon).toHaveAttribute('data-icon', 'medal')
+ expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge')
+ })
+
+ it('renders fallback fa-medal for invalid cssClass', () => {
+ render()
+
+ const icon = screen.getByTestId('badge-icon')
+ expect(icon).toBeInTheDocument()
+ expect(icon).toHaveAttribute('data-icon', 'medal')
+ })
+
+ it('renders fallback medal for unrecognized icon', () => {
+ render()
+
+ const icon = screen.getByTestId('badge-icon')
+ expect(icon).toBeInTheDocument()
+ expect(icon).toHaveAttribute('data-icon', 'medal')
+ })
+
+ it('hides tooltip when showTooltip is false', () => {
+ render()
+
+ const icon = screen.getByTestId('badge-icon')
+ expect(icon).toBeInTheDocument()
+ expect(icon).toHaveAttribute('data-icon', 'medal')
+ expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument()
+ })
+
+ describe('Backend enum icons', () => {
+ const backendIcons = [
+ { cssClass: 'award', expectedIcon: 'award' },
+ { cssClass: 'medal', expectedIcon: 'medal' },
+ { cssClass: 'ribbon', expectedIcon: 'ribbon' },
+ { cssClass: 'star', expectedIcon: 'star' },
+ { cssClass: 'certificate', expectedIcon: 'certificate' },
+ { cssClass: 'bug_slash', expectedIcon: 'bug' }, // Backend snake_case input
+ ]
+
+ backendIcons.forEach(({ cssClass, expectedIcon }) => {
+ it(`renders ${cssClass} icon correctly (transforms snake_case to camelCase)`, () => {
+ render()
+
+ const icon = screen.getByTestId('badge-icon')
+ expect(icon).toBeInTheDocument()
+ expect(icon).toHaveAttribute('data-icon', expectedIcon)
+ })
+ })
+
+ it('handles camelCase input directly', () => {
+ render()
+
+ const icon = screen.getByTestId('badge-icon')
+ expect(icon).toBeInTheDocument()
+ expect(icon).toHaveAttribute('data-icon', 'bug')
+ })
+ })
+})
diff --git a/frontend/__tests__/unit/components/UserCard.test.tsx b/frontend/__tests__/unit/components/UserCard.test.tsx
index a75505af28..7a57af134a 100644
--- a/frontend/__tests__/unit/components/UserCard.test.tsx
+++ b/frontend/__tests__/unit/components/UserCard.test.tsx
@@ -90,6 +90,7 @@ describe('UserCard', () => {
followersCount: 0,
location: '',
repositoriesCount: 0,
+ badgeCount: 0,
}
beforeEach(() => {
@@ -120,6 +121,7 @@ describe('UserCard', () => {
followersCount: 1500,
location: 'San Francisco, CA',
repositoriesCount: 25,
+ badgeCount: 5,
button: {
label: 'View Profile',
onclick: mockButtonClick,
@@ -379,4 +381,41 @@ describe('UserCard', () => {
expect(screen.getByText('5.7k')).toBeInTheDocument()
})
})
+
+ describe('Badge Count Display', () => {
+ it('renders badge count when greater than 0', () => {
+ render()
+
+ expect(screen.getByTestId('icon-medal')).toBeInTheDocument()
+ expect(screen.getByText('5')).toBeInTheDocument()
+ })
+
+ it('does not render badge count when 0', () => {
+ render()
+
+ expect(screen.queryByTestId('icon-medal')).not.toBeInTheDocument()
+ })
+
+ it('renders all three metrics when all are greater than 0', () => {
+ render(
+
+ )
+
+ expect(screen.getByTestId('icon-users')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-folder-open')).toBeInTheDocument()
+ expect(screen.getByTestId('icon-medal')).toBeInTheDocument()
+ })
+
+ it('formats badge count with millify for large numbers', () => {
+ render()
+
+ expect(screen.getByText('1.5k')).toBeInTheDocument()
+ })
+
+ it('handles negative badge count', () => {
+ render()
+
+ expect(screen.queryByTestId('icon-medal')).not.toBeInTheDocument()
+ })
+ })
})
diff --git a/frontend/__tests__/unit/data/mockBadgeData.ts b/frontend/__tests__/unit/data/mockBadgeData.ts
new file mode 100644
index 0000000000..afde137fd2
--- /dev/null
+++ b/frontend/__tests__/unit/data/mockBadgeData.ts
@@ -0,0 +1,63 @@
+import type { Badge } from 'types/badge'
+
+export const mockBadgeData: Badge[] = [
+ {
+ id: '1',
+ name: 'Contributor',
+ cssClass: 'fa-medal',
+ description: 'Active contributor to OWASP projects',
+ weight: 1,
+ },
+ {
+ id: '2',
+ name: 'Security Expert',
+ cssClass: 'fa-shield-alt',
+ description: 'Security expertise demonstrated',
+ weight: 2,
+ },
+ {
+ id: '3',
+ name: 'Code Reviewer',
+ cssClass: 'fa-code',
+ description: 'Regular code reviewer',
+ weight: 1,
+ },
+ {
+ id: '4',
+ name: 'Mentor',
+ cssClass: 'fa-user-graduate',
+ description: 'Mentors other contributors',
+ weight: 3,
+ },
+ {
+ id: '5',
+ name: 'Project Lead',
+ cssClass: 'fa-crown',
+ description: 'Leads OWASP projects',
+ weight: 4,
+ },
+]
+
+export const mockUserBadgeQueryResponse = {
+ user: {
+ id: '1',
+ login: 'testuser',
+ name: 'Test User',
+ badges: mockBadgeData,
+ badgeCount: 5,
+ },
+}
+
+export const mockUserWithoutBadgeQueryResponse = {
+ user: {
+ id: '2',
+ login: 'testuser2',
+ name: 'Test User 2',
+ badges: [],
+ badgeCount: 0,
+ },
+}
+
+export const mockBadgeQueryResponse = {
+ badges: mockBadgeData,
+}
diff --git a/frontend/__tests__/unit/data/mockUserDetails.ts b/frontend/__tests__/unit/data/mockUserDetails.ts
index a082f47170..5082584541 100644
--- a/frontend/__tests__/unit/data/mockUserDetails.ts
+++ b/frontend/__tests__/unit/data/mockUserDetails.ts
@@ -13,6 +13,23 @@ export const mockUserDetailsData = {
publicRepositoriesCount: 3,
createdAt: 1723002473,
contributionsCount: 100,
+ badges: [
+ {
+ id: '1',
+ name: 'Contributor',
+ cssClass: 'fa-medal',
+ description: 'Active contributor to OWASP projects',
+ weight: 1,
+ },
+ {
+ id: '2',
+ name: 'Security Expert',
+ cssClass: 'fa-shield-alt',
+ description: 'Security expertise demonstrated',
+ weight: 2,
+ },
+ ],
+ badgeCount: 2,
},
recentIssues: [
{
diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx
index 1f8eb46038..89498478f5 100644
--- a/frontend/__tests__/unit/pages/UserDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx
@@ -15,9 +15,46 @@ jest.mock('@apollo/client/react', () => ({
// Mock FontAwesome
jest.mock('@fortawesome/react-fontawesome', () => ({
- FontAwesomeIcon: () => ,
+ FontAwesomeIcon: ({
+ icon,
+ className,
+ ...props
+ }: {
+ icon: string[] | { iconName: string }
+ className?: string
+ [key: string]: unknown
+ }) => {
+ const iconName = Array.isArray(icon) ? icon[1] : icon.iconName
+ return
+ },
}))
+// Mock Badges component
+jest.mock('components/Badges', () => {
+ const MockBadges = ({
+ name,
+ cssClass,
+ showTooltip,
+ }: {
+ name: string
+ cssClass: string
+ showTooltip?: boolean
+ }) => (
+
+
+
+ )
+ MockBadges.displayName = 'MockBadges'
+ return {
+ __esModule: true,
+ default: MockBadges,
+ }
+})
+
const mockRouter = {
push: jest.fn(),
}
@@ -29,7 +66,7 @@ const mockError = {
jest.mock('next/navigation', () => ({
...jest.requireActual('next/navigation'),
useRouter: jest.fn(() => mockRouter),
- useParams: () => ({ userKey: 'test-user' }),
+ useParams: () => ({ memberKey: 'test-user' }),
}))
// Mock GitHub heatmap utilities
@@ -498,4 +535,275 @@ describe('UserDetailsPage', () => {
expect(screen.queryByText(`Want to become a sponsor?`)).toBeNull()
})
})
+
+ describe('Badge Display Tests', () => {
+ test('renders badges section when user has badges', async () => {
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: mockUserDetailsData,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ expect(screen.getByTestId('badge-contributor')).toBeInTheDocument()
+ expect(screen.getByTestId('badge-security-expert')).toBeInTheDocument()
+ })
+ })
+
+ test('renders badges with correct props', async () => {
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: mockUserDetailsData,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ const contributorBadge = screen.getByTestId('badge-contributor')
+ expect(contributorBadge).toHaveAttribute('data-css-class', 'fa-medal')
+ expect(contributorBadge).toHaveAttribute('data-show-tooltip', 'true')
+
+ const securityBadge = screen.getByTestId('badge-security-expert')
+ expect(securityBadge).toHaveAttribute('data-css-class', 'fa-shield-alt')
+ expect(securityBadge).toHaveAttribute('data-show-tooltip', 'true')
+ })
+ })
+
+ test('does not render badges section when user has no badges', async () => {
+ const dataWithoutBadges = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ badges: [],
+ badgeCount: 0,
+ },
+ }
+
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithoutBadges,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ expect(screen.queryByTestId(/^badge-/)).not.toBeInTheDocument()
+ })
+ })
+
+ test('does not render badges section when badges is undefined', async () => {
+ const dataWithoutBadges = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ badges: undefined,
+ badgeCount: 0,
+ },
+ }
+
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithoutBadges,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ expect(screen.queryByTestId(/^badge-/)).not.toBeInTheDocument()
+ })
+ })
+
+ test('renders badges with fallback cssClass when not provided', async () => {
+ const dataWithIncompleteBadges = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ badges: [
+ {
+ id: '1',
+ name: 'Test Badge',
+ cssClass: undefined,
+ description: 'Test description',
+ weight: 1,
+ },
+ ],
+ },
+ }
+
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithIncompleteBadges,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ const badge = screen.getByTestId('badge-test-badge')
+ expect(badge).toHaveAttribute('data-css-class', 'fa-medal')
+ })
+ })
+
+ test('renders badges with empty cssClass fallback', async () => {
+ const dataWithEmptyCssClass = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ badges: [
+ {
+ id: '1',
+ name: 'Test Badge',
+ cssClass: '',
+ description: 'Test description',
+ weight: 1,
+ },
+ ],
+ },
+ }
+
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithEmptyCssClass,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ const badge = screen.getByTestId('badge-test-badge')
+ expect(badge).toHaveAttribute('data-css-class', 'fa-medal')
+ })
+ })
+
+ test('handles badges with special characters in names', async () => {
+ const dataWithSpecialBadges = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ badges: [
+ {
+ id: '1',
+ name: 'Badge & More!',
+ cssClass: 'fa-star',
+ description: 'Special badge',
+ weight: 1,
+ },
+ ],
+ },
+ }
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithSpecialBadges,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ expect(screen.getByTestId('badge-badge-&-more!')).toBeInTheDocument()
+ })
+ })
+
+ test('handles badges with long names', async () => {
+ const dataWithLongNameBadge = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ badges: [
+ {
+ id: '1',
+ name: 'Very Long Badge Name That Exceeds Normal Length',
+ cssClass: 'fa-trophy',
+ description: 'Long name badge',
+ weight: 1,
+ },
+ ],
+ },
+ }
+
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithLongNameBadge,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ expect(
+ screen.getByTestId('badge-very-long-badge-name-that-exceeds-normal-length')
+ ).toBeInTheDocument()
+ })
+ })
+
+ test('renders badges in correct order as returned by backend (weight ASC then name ASC)', async () => {
+ // Backend returns badges sorted by weight ASC, then name ASC
+ // This test verifies the frontend preserves the backend ordering
+ const dataWithOrderedBadges = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ badges: [
+ // Backend returns badges in this order: weight ASC, then name ASC
+ {
+ id: '3',
+ name: 'Alpha Badge',
+ cssClass: 'fa-star',
+ description: 'Alpha badge with weight 1',
+ weight: 1,
+ },
+ {
+ id: '4',
+ name: 'Beta Badge',
+ cssClass: 'fa-trophy',
+ description: 'Beta badge with weight 1',
+ weight: 1,
+ },
+ {
+ id: '1',
+ name: 'Contributor',
+ cssClass: 'fa-medal',
+ description: 'Active contributor',
+ weight: 1,
+ },
+ {
+ id: '2',
+ name: 'Security Expert',
+ cssClass: 'fa-shield-alt',
+ description: 'Security expertise',
+ weight: 2,
+ },
+ {
+ id: '5',
+ name: 'Top Contributor',
+ cssClass: 'fa-crown',
+ description: 'Highest weight badge',
+ weight: 3,
+ },
+ ],
+ badgeCount: 5,
+ },
+ }
+
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithOrderedBadges,
+ loading: false,
+ error: null,
+ })
+
+ render()
+ await waitFor(() => {
+ const badgeElements = screen.getAllByTestId(/^badge-/)
+ const badgeTestIds = badgeElements.map((element) => element.getAttribute('data-testid'))
+
+ // Expected order matches backend contract: weight ASC (1, 1, 1, 2, 3), then name ASC for equal weights
+ const expectedOrder = [
+ 'badge-alpha-badge', // weight 1, name ASC
+ 'badge-beta-badge', // weight 1, name ASC
+ 'badge-contributor', // weight 1, name ASC
+ 'badge-security-expert', // weight 2
+ 'badge-top-contributor', // weight 3
+ ]
+
+ expect(badgeTestIds).toEqual(expectedOrder)
+ })
+ })
+ })
})
diff --git a/frontend/package.json b/frontend/package.json
index e544dd8811..cce6b0570c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -77,7 +77,7 @@
"@graphql-codegen/typescript-operations": "^5.0.2",
"@lhci/cli": "^0.15.1",
"@playwright/test": "^1.56.0",
- "@swc/core": "^1.13.20",
+ "@swc/core": "1.13.19",
"@swc/jest": "^0.2.39",
"@tailwindcss/postcss": "^4.1.14",
"@testing-library/jest-dom": "^6.9.1",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index bca3b89d68..b03b459a0d 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -64,7 +64,7 @@ importers:
version: 15.5.4(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
'@sentry/nextjs':
specifier: ^10.19.0
- version: 10.19.0(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17)))
+ version: 10.19.0(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17)))
'@testing-library/user-event':
specifier: ^14.6.1
version: 14.6.1(@testing-library/dom@10.4.1)
@@ -181,11 +181,11 @@ importers:
specifier: ^1.56.0
version: 1.56.0
'@swc/core':
- specifier: ^1.13.20
- version: 1.13.20(@swc/helpers@0.5.17)
+ specifier: 1.13.19
+ version: 1.13.19(@swc/helpers@0.5.17)
'@swc/jest':
specifier: ^0.2.39
- version: 0.2.39(@swc/core@1.13.20(@swc/helpers@0.5.17))
+ version: 0.2.39(@swc/core@1.13.19(@swc/helpers@0.5.17))
'@tailwindcss/postcss':
specifier: ^4.1.14
version: 4.1.14
@@ -239,7 +239,7 @@ importers:
version: 1.1.2(eslint-plugin-import@2.32.0)
eslint-plugin-jest:
specifier: ^29.0.1
- version: 29.0.1(@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3)
+ version: 29.0.1(@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3)
eslint-plugin-jsx-a11y:
specifier: ^6.10.2
version: 6.10.2(eslint@9.37.0(jiti@2.6.1))
@@ -263,7 +263,7 @@ importers:
version: 1.15.0
jest:
specifier: ^30.2.0
- version: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
+ version: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
jest-axe:
specifier: ^10.0.0
version: 10.0.0
@@ -290,10 +290,10 @@ importers:
version: 4.1.14
ts-jest:
specifier: ^29.4.5
- version: 29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3)
+ version: 29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3)
ts-node:
specifier: ^10.9.2
- version: 10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)
+ version: 10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)
typescript:
specifier: ~5.9.3
version: 5.9.3
@@ -3174,68 +3174,68 @@ packages:
peerDependencies:
'@svgdotjs/svg.js': ^3.2.4
- '@swc/core-darwin-arm64@1.13.20':
- resolution: {integrity: sha512-k/nqRwm6G3tw1BbCDxc3KmAMGsuDYA5Uh4MjYm23e+UziLyHz0z7W0zja3el+yGBIZXKlgSzWVFLsFDFzVqtgg==}
+ '@swc/core-darwin-arm64@1.13.19':
+ resolution: {integrity: sha512-NxDyte9tCJSJ8+R62WDtqwg8eI57lubD52sHyGOfezpJBOPr36bUSGGLyO3Vod9zTGlOu2CpkuzA/2iVw92u1g==}
engines: {node: '>=10'}
cpu: [arm64]
os: [darwin]
- '@swc/core-darwin-x64@1.13.20':
- resolution: {integrity: sha512-7xr+ACdUMNyrN87oEF1GvJIZJBAhGolfQVB0EYP08JEy8VSh//FEwfdlUz8gweaZyjOl1nuPS6ncXlKgZuZU8A==}
+ '@swc/core-darwin-x64@1.13.19':
+ resolution: {integrity: sha512-+w5DYrJndSygFFRDcuPYmx5BljD6oYnAohZ15K1L6SfORHp/BTSIbgSFRKPoyhjuIkDiq3W0um8RoMTOBAcQjQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [darwin]
- '@swc/core-linux-arm-gnueabihf@1.13.20':
- resolution: {integrity: sha512-IaOLxU1U/oGV3lZ2T8tD5nB/5O60UFPqj5ZxYzDpCBVB73tDQDIxiDcro1X81nHbwJHjuHmbIrhoflS7LQN6+A==}
+ '@swc/core-linux-arm-gnueabihf@1.13.19':
+ resolution: {integrity: sha512-7LlfgpdwwYq2q7himNkAAFo4q6jysMLFNoBH6GRP7WL29NcSsl5mPMJjmYZymK+sYq/9MTVieDTQvChzYDsapw==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux]
- '@swc/core-linux-arm64-gnu@1.13.20':
- resolution: {integrity: sha512-Lg6FyotDydXGnNnlw+u7vCZzR2+fX3Q2HiULBTYl2dey3TvRyzAfEhdgMjUc4beRzf26U9rzMuTroJ6KMBCBjA==}
+ '@swc/core-linux-arm64-gnu@1.13.19':
+ resolution: {integrity: sha512-ml3I6Lm2marAQ3UC/TS9t/yILBh/eDSVHAdPpikp652xouWAVW1znUeV6bBSxe1sSZIenv+p55ubKAWq/u84sQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
- '@swc/core-linux-arm64-musl@1.13.20':
- resolution: {integrity: sha512-d1SvxmFykS0Ep8nPbduV1UwCvFjZ3ESzFKQdTbkr72bge8AseILBI9TbLTmoeWndDaTesiiTKRD5Y1iAvF1wvA==}
+ '@swc/core-linux-arm64-musl@1.13.19':
+ resolution: {integrity: sha512-M/otFc3/rWWkbF6VgbOXVzUKVoE7MFcphTaStxJp4bwb7oP5slYlxMZN51Dk/OTOfvCDo9pTAFDKNyixbkXMDQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
- '@swc/core-linux-x64-gnu@1.13.20':
- resolution: {integrity: sha512-Bwmng57EuMod58Q8GDJA8rmUgFl20taK8w8MqeeDMiCnZY2+rJrNERbIX3sXZbwsf/kCIELZ7q4ZXiwdyB4zoQ==}
+ '@swc/core-linux-x64-gnu@1.13.19':
+ resolution: {integrity: sha512-NoMUKaOJEdouU4tKF88ggdDHFiRRING+gYLxDqnTfm+sUXaizB5OGBRzvSVDYSXQb1SuUuChnXFPFzwTWbt3ZQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
- '@swc/core-linux-x64-musl@1.13.20':
- resolution: {integrity: sha512-osCm3VEKL/OIKInyhy75S5B+R+QGBdpR1B5vwTYqG/1RB4vFM3O5SDtRZabd6NV9Cxc9dcLztWyZjhs2qp63SQ==}
+ '@swc/core-linux-x64-musl@1.13.19':
+ resolution: {integrity: sha512-r6krlZwyu8SBaw24QuS1lau2I9q8M+eJV6ITz0rpb6P1Bx0elf9ii5Bhh8ddmIqXXH8kOGSjC/dwcdHbZqAhgw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
- '@swc/core-win32-arm64-msvc@1.13.20':
- resolution: {integrity: sha512-svbQNirwEa6zwaAJPrEmQnMVZsOz8Jpr4nakFLkYIQwwJ73sBUkUJvH9ouIWmIu5bvgQrbQlRpxWTIY3e0Utlg==}
+ '@swc/core-win32-arm64-msvc@1.13.19':
+ resolution: {integrity: sha512-awcZSIuxyVn0Dw28VjMvgk1qiDJ6CeQwHkZNUjg2UxVlq23zE01NMMp+zkoGFypmLG9gaGmJSzuoqvk/WCQ5tw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
- '@swc/core-win32-ia32-msvc@1.13.20':
- resolution: {integrity: sha512-uVjjwGXJltUQK0v1qQSNGeMS6osLJuwgeTti5N7kxQ6mOfa1irxq+TX0YdIVQwIONMjzI+TP7lhqPeA9VdUjRg==}
+ '@swc/core-win32-ia32-msvc@1.13.19':
+ resolution: {integrity: sha512-H5d+KO7ISoLNgYvTbOcCQjJZNM3R7yaYlrMAF13lUr6GSiOUX+92xtM31B+HvzAWI7HtvVe74d29aC1b1TpXFA==}
engines: {node: '>=10'}
cpu: [ia32]
os: [win32]
- '@swc/core-win32-x64-msvc@1.13.20':
- resolution: {integrity: sha512-Xm1JAew/P0TgsPSXyo60IH865fAmt9b2Mzd0FBJ77Q1xA1o/Oi9teCeGChyFq3+6JFao6uT0N4mcI3BJ4WBfkA==}
+ '@swc/core-win32-x64-msvc@1.13.19':
+ resolution: {integrity: sha512-qNoyCpXvv2O3JqXKanRIeoMn03Fho/As+N4Fhe7u0FsYh4VYqGQah4DGDzEP/yjl4Gx1IElhqLGDhCCGMwWaDw==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
- '@swc/core@1.13.20':
- resolution: {integrity: sha512-w6REE95NkGhQH/baA0reb6IQjVzSy5HOz9bZnRTFgOv+a1ZDo4p6yVs4McpFOZJeu810DSHayO3mwBsBXxZcaw==}
+ '@swc/core@1.13.19':
+ resolution: {integrity: sha512-V1r4wFdjaZIUIZZrV2Mb/prEeu03xvSm6oatPxsvnXKF9lNh5Jtk9QvUdiVfD9rrvi7bXrAVhg9Wpbmv/2Fl1g==}
engines: {node: '>=10'}
peerDependencies:
'@swc/helpers': '>=0.5.17'
@@ -10275,7 +10275,7 @@ snapshots:
jest-util: 30.2.0
slash: 3.0.0
- '@jest/core@30.2.0(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))':
+ '@jest/core@30.2.0(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))':
dependencies:
'@jest/console': 30.2.0
'@jest/pattern': 30.0.1
@@ -10290,7 +10290,7 @@ snapshots:
exit-x: 0.2.2
graceful-fs: 4.2.11
jest-changed-files: 30.2.0
- jest-config: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
+ jest-config: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
jest-haste-map: 30.2.0
jest-message-util: 30.2.0
jest-regex-util: 30.0.1
@@ -11851,7 +11851,7 @@ snapshots:
'@sentry/utils': 7.120.4
localforage: 1.10.0
- '@sentry/nextjs@10.19.0(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17)))':
+ '@sentry/nextjs@10.19.0(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(next@15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17)))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.37.0
@@ -11863,7 +11863,7 @@ snapshots:
'@sentry/opentelemetry': 10.19.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0)
'@sentry/react': 10.19.0(react@19.2.0)
'@sentry/vercel-edge': 10.19.0
- '@sentry/webpack-plugin': 4.4.0(webpack@5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17)))
+ '@sentry/webpack-plugin': 4.4.0(webpack@5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17)))
chalk: 3.0.0
next: 15.5.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
resolve: 1.22.8
@@ -11970,12 +11970,12 @@ snapshots:
'@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0)
'@sentry/core': 10.19.0
- '@sentry/webpack-plugin@4.4.0(webpack@5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17)))':
+ '@sentry/webpack-plugin@4.4.0(webpack@5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17)))':
dependencies:
'@sentry/bundler-plugin-core': 4.4.0
unplugin: 1.0.1
uuid: 9.0.1
- webpack: 5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17))
+ webpack: 5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17))
transitivePeerDependencies:
- encoding
- supports-color
@@ -12011,51 +12011,51 @@ snapshots:
dependencies:
'@svgdotjs/svg.js': 3.2.5
- '@swc/core-darwin-arm64@1.13.20':
+ '@swc/core-darwin-arm64@1.13.19':
optional: true
- '@swc/core-darwin-x64@1.13.20':
+ '@swc/core-darwin-x64@1.13.19':
optional: true
- '@swc/core-linux-arm-gnueabihf@1.13.20':
+ '@swc/core-linux-arm-gnueabihf@1.13.19':
optional: true
- '@swc/core-linux-arm64-gnu@1.13.20':
+ '@swc/core-linux-arm64-gnu@1.13.19':
optional: true
- '@swc/core-linux-arm64-musl@1.13.20':
+ '@swc/core-linux-arm64-musl@1.13.19':
optional: true
- '@swc/core-linux-x64-gnu@1.13.20':
+ '@swc/core-linux-x64-gnu@1.13.19':
optional: true
- '@swc/core-linux-x64-musl@1.13.20':
+ '@swc/core-linux-x64-musl@1.13.19':
optional: true
- '@swc/core-win32-arm64-msvc@1.13.20':
+ '@swc/core-win32-arm64-msvc@1.13.19':
optional: true
- '@swc/core-win32-ia32-msvc@1.13.20':
+ '@swc/core-win32-ia32-msvc@1.13.19':
optional: true
- '@swc/core-win32-x64-msvc@1.13.20':
+ '@swc/core-win32-x64-msvc@1.13.19':
optional: true
- '@swc/core@1.13.20(@swc/helpers@0.5.17)':
+ '@swc/core@1.13.19(@swc/helpers@0.5.17)':
dependencies:
'@swc/counter': 0.1.3
'@swc/types': 0.1.25
optionalDependencies:
- '@swc/core-darwin-arm64': 1.13.20
- '@swc/core-darwin-x64': 1.13.20
- '@swc/core-linux-arm-gnueabihf': 1.13.20
- '@swc/core-linux-arm64-gnu': 1.13.20
- '@swc/core-linux-arm64-musl': 1.13.20
- '@swc/core-linux-x64-gnu': 1.13.20
- '@swc/core-linux-x64-musl': 1.13.20
- '@swc/core-win32-arm64-msvc': 1.13.20
- '@swc/core-win32-ia32-msvc': 1.13.20
- '@swc/core-win32-x64-msvc': 1.13.20
+ '@swc/core-darwin-arm64': 1.13.19
+ '@swc/core-darwin-x64': 1.13.19
+ '@swc/core-linux-arm-gnueabihf': 1.13.19
+ '@swc/core-linux-arm64-gnu': 1.13.19
+ '@swc/core-linux-arm64-musl': 1.13.19
+ '@swc/core-linux-x64-gnu': 1.13.19
+ '@swc/core-linux-x64-musl': 1.13.19
+ '@swc/core-win32-arm64-msvc': 1.13.19
+ '@swc/core-win32-ia32-msvc': 1.13.19
+ '@swc/core-win32-x64-msvc': 1.13.19
'@swc/helpers': 0.5.17
'@swc/counter@0.1.3': {}
@@ -12068,10 +12068,10 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@swc/jest@0.2.39(@swc/core@1.13.20(@swc/helpers@0.5.17))':
+ '@swc/jest@0.2.39(@swc/core@1.13.19(@swc/helpers@0.5.17))':
dependencies:
'@jest/create-cache-key-function': 30.2.0
- '@swc/core': 1.13.20(@swc/helpers@0.5.17)
+ '@swc/core': 1.13.19(@swc/helpers@0.5.17)
'@swc/counter': 0.1.3
jsonc-parser: 3.3.1
@@ -13749,13 +13749,13 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-jest@29.0.1(@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3):
+ eslint-plugin-jest@29.0.1(@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3):
dependencies:
'@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.37.0(jiti@2.6.1)
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)
- jest: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
+ jest: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
transitivePeerDependencies:
- supports-color
- typescript
@@ -14771,15 +14771,15 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-cli@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)):
+ jest-cli@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)):
dependencies:
- '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
+ '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
'@jest/test-result': 30.2.0
'@jest/types': 30.2.0
chalk: 4.1.2
exit-x: 0.2.2
import-local: 3.2.0
- jest-config: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
+ jest-config: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
jest-util: 30.2.0
jest-validate: 30.2.0
yargs: 17.7.2
@@ -14790,7 +14790,7 @@ snapshots:
- supports-color
- ts-node
- jest-config@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)):
+ jest-config@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.28.4
'@jest/get-type': 30.1.0
@@ -14818,7 +14818,7 @@ snapshots:
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.18.10
- ts-node: 10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)
+ ts-node: 10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@@ -15072,12 +15072,12 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
- jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)):
+ jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)):
dependencies:
- '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
+ '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
'@jest/types': 30.2.0
import-local: 3.2.0
- jest-cli: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
+ jest-cli: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -16662,16 +16662,16 @@ snapshots:
minizlib: 3.1.0
yallist: 5.0.0
- terser-webpack-plugin@5.3.14(@swc/core@1.13.20(@swc/helpers@0.5.17))(webpack@5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17))):
+ terser-webpack-plugin@5.3.14(@swc/core@1.13.19(@swc/helpers@0.5.17))(webpack@5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17))):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
serialize-javascript: 6.0.2
terser: 5.44.0
- webpack: 5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17))
+ webpack: 5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17))
optionalDependencies:
- '@swc/core': 1.13.20(@swc/helpers@0.5.17)
+ '@swc/core': 1.13.19(@swc/helpers@0.5.17)
terser@5.44.0:
dependencies:
@@ -16753,12 +16753,12 @@ snapshots:
dependencies:
typescript: 5.9.3
- ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3):
+ ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3)))(typescript@5.9.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
handlebars: 4.7.8
- jest: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
+ jest: 30.2.0(@types/node@22.18.10)(ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3))
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
@@ -16775,7 +16775,7 @@ snapshots:
ts-log@2.2.7: {}
- ts-node@10.9.2(@swc/core@1.13.20(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3):
+ ts-node@10.9.2(@swc/core@1.13.19(@swc/helpers@0.5.17))(@types/node@22.18.10)(typescript@5.9.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
@@ -16793,7 +16793,7 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
- '@swc/core': 1.13.20(@swc/helpers@0.5.17)
+ '@swc/core': 1.13.19(@swc/helpers@0.5.17)
tsconfig-paths@3.15.0:
dependencies:
@@ -17030,7 +17030,7 @@ snapshots:
webpack-virtual-modules@0.5.0: {}
- webpack@5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17)):
+ webpack@5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17)):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8
@@ -17054,7 +17054,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 4.3.3
tapable: 2.3.0
- terser-webpack-plugin: 5.3.14(@swc/core@1.13.20(@swc/helpers@0.5.17))(webpack@5.101.3(@swc/core@1.13.20(@swc/helpers@0.5.17)))
+ terser-webpack-plugin: 5.3.14(@swc/core@1.13.19(@swc/helpers@0.5.17))(webpack@5.101.3(@swc/core@1.13.19(@swc/helpers@0.5.17)))
watchpack: 2.4.4
webpack-sources: 3.3.3
transitivePeerDependencies:
diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx
index 094bf53ddc..5936f9609f 100644
--- a/frontend/src/app/members/[memberKey]/page.tsx
+++ b/frontend/src/app/members/[memberKey]/page.tsx
@@ -12,7 +12,9 @@ import { useParams } from 'next/navigation'
import { useTheme } from 'next-themes'
import React, { useState, useEffect, useRef } from 'react'
import { handleAppError, ErrorDisplay } from 'app/global-error'
+
import { GetUserDataDocument } from 'types/__generated__/userQueries.generated'
+import { Badge } from 'types/badge'
import type { Issue } from 'types/issue'
import type { Milestone } from 'types/milestone'
import type { RepositoryCardProps } from 'types/project'
@@ -21,6 +23,7 @@ import type { Release } from 'types/release'
import type { User } from 'types/user'
import { formatDate } from 'utils/dateFormatter'
import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/githubHeatmap'
+import Badges from 'components/Badges'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
@@ -185,24 +188,39 @@ const UserDetailsPage: React.FC = () => {
}
const UserSummary = () => (
-
-
-
-
-
-
-
- @{user?.login}
-
+
+
+
+
+
+
+ @{user?.login}
+
+ {user?.badges && user.badges.length > 0 && (
+
+ {user.badges.slice().map((badge: Badge) => (
+
+
+
+ ))}
+
+ )}
+
{formattedBio}
-
{!isPrivateContributor && (
diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx
index 955210d6d6..88ca788835 100644
--- a/frontend/src/app/members/page.tsx
+++ b/frontend/src/app/members/page.tsx
@@ -22,7 +22,6 @@ const UsersPage = () => {
})
const router = useRouter()
-
const handleButtonClick = (user: User) => {
router.push(`/members/${user.key}`)
}
@@ -34,9 +33,12 @@ const UsersPage = () => {
onclick: () => handleButtonClick(user),
}
+ const badgeCount = user.badgeCount || 0
return (
{
+ if (!cssClass || cssClass.trim() === '') {
+ return ''
+ }
+
+ // Convert backend snake_case format to frontend camelCase format
+ return cssClass.trim().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
+}
+
+const resolveIcon = (cssClass: string | undefined) => {
+ const normalizedClass = normalizeCssClass(cssClass)
+ return BADGE_CLASS_MAP[normalizedClass] ?? DEFAULT_ICON
+}
+
+const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => {
+ const icon = resolveIcon(cssClass)
+
+ return (
+
+
+
+
+
+ )
+}
+
+export default Badges
diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx
index 4df1aec7b6..34ed8b1f77 100644
--- a/frontend/src/components/UserCard.tsx
+++ b/frontend/src/components/UserCard.tsx
@@ -1,4 +1,10 @@
-import { faChevronRight, faFolderOpen, faUser, faUsers } from '@fortawesome/free-solid-svg-icons'
+import {
+ faChevronRight,
+ faFolderOpen,
+ faMedal,
+ faUser,
+ faUsers,
+} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Button } from '@heroui/button'
import millify from 'millify'
@@ -7,6 +13,7 @@ import type { UserCardProps } from 'types/card'
const UserCard = ({
avatar,
+ badgeCount,
button,
className,
company,
@@ -62,6 +69,12 @@ const UserCard = ({
{millify(repositoriesCount, { precision: 1 })}
)}
+ {badgeCount > 0 && (
+
+
+ {millify(badgeCount, { precision: 1 })}{' '}
+
+ )}
diff --git a/frontend/src/server/queries/userQueries.ts b/frontend/src/server/queries/userQueries.ts
index dd9c24cb76..73414bb66e 100644
--- a/frontend/src/server/queries/userQueries.ts
+++ b/frontend/src/server/queries/userQueries.ts
@@ -7,6 +7,14 @@ export const GET_LEADER_DATA = gql`
avatarUrl
login
name
+ badgeCount
+ badges {
+ cssClass
+ description
+ id
+ name
+ weight
+ }
}
}
`
@@ -65,8 +73,15 @@ export const GET_USER_DATA = gql`
url
}
user(login: $key) {
- id
avatarUrl
+ badgeCount
+ badges {
+ cssClass
+ description
+ id
+ name
+ weight
+ }
bio
company
contributionsCount
@@ -74,6 +89,7 @@ export const GET_USER_DATA = gql`
email
followersCount
followingCount
+ id
issuesCount
location
login
@@ -88,8 +104,9 @@ export const GET_USER_DATA = gql`
export const GET_USER_METADATA = gql`
query GetUserMetadata($key: String!) {
user(login: $key) {
- id
+ badgeCount
bio
+ id
login
name
}
diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts
index bf05f4842f..16a0815080 100644
--- a/frontend/src/types/__generated__/graphql.ts
+++ b/frontend/src/types/__generated__/graphql.ts
@@ -877,6 +877,7 @@ export type UpdateProgramStatusInput = {
export type UserNode = {
__typename?: 'UserNode';
avatarUrl: Scalars['String']['output'];
+ badgeCount: Scalars['Int']['output'];
badges: Array
;
bio: Scalars['String']['output'];
company: Scalars['String']['output'];
diff --git a/frontend/src/types/__generated__/userQueries.generated.ts b/frontend/src/types/__generated__/userQueries.generated.ts
index 8516cb0dae..c590328d1b 100644
--- a/frontend/src/types/__generated__/userQueries.generated.ts
+++ b/frontend/src/types/__generated__/userQueries.generated.ts
@@ -6,23 +6,23 @@ export type GetLeaderDataQueryVariables = Types.Exact<{
}>;
-export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null };
+export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, badgeCount: number, badges: Array<{ __typename: 'BadgeNode', cssClass: string, description: string, id: string, name: string, weight: number }> } | null };
export type GetUserDataQueryVariables = Types.Exact<{
key: Types.Scalars['String']['input'];
}>;
-export type GetUserDataQuery = { recentIssues: Array<{ __typename: 'IssueNode', id: string, createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentMilestones: Array<{ __typename: 'MilestoneNode', id: string, title: string, openIssuesCount: number, closedIssuesCount: number, repositoryName: string | null, organizationName: string | null, createdAt: any, url: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, publishedAt: any | null, organizationName: string | null, repositoryName: string | null, tagName: string, url: string }>, topContributedRepositories: Array<{ __typename: 'RepositoryNode', id: string, contributorsCount: number, forksCount: number, key: string, name: string, openIssuesCount: number, starsCount: number, subscribersCount: number, url: string, organization: { __typename: 'OrganizationNode', id: string, login: string } | null }>, user: { __typename: 'UserNode', id: string, avatarUrl: string, bio: string, company: string, contributionsCount: number, createdAt: number, email: string, followersCount: number, followingCount: number, issuesCount: number, location: string, login: string, name: string, publicRepositoriesCount: number, releasesCount: number, updatedAt: number, url: string } | null };
+export type GetUserDataQuery = { recentIssues: Array<{ __typename: 'IssueNode', id: string, createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentMilestones: Array<{ __typename: 'MilestoneNode', id: string, title: string, openIssuesCount: number, closedIssuesCount: number, repositoryName: string | null, organizationName: string | null, createdAt: any, url: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, publishedAt: any | null, organizationName: string | null, repositoryName: string | null, tagName: string, url: string }>, topContributedRepositories: Array<{ __typename: 'RepositoryNode', id: string, contributorsCount: number, forksCount: number, key: string, name: string, openIssuesCount: number, starsCount: number, subscribersCount: number, url: string, organization: { __typename: 'OrganizationNode', id: string, login: string } | null }>, user: { __typename: 'UserNode', avatarUrl: string, badgeCount: number, bio: string, company: string, contributionsCount: number, createdAt: number, email: string, followersCount: number, followingCount: number, id: string, issuesCount: number, location: string, login: string, name: string, publicRepositoriesCount: number, releasesCount: number, updatedAt: number, url: string, badges: Array<{ __typename: 'BadgeNode', cssClass: string, description: string, id: string, name: string, weight: number }> } | null };
export type GetUserMetadataQueryVariables = Types.Exact<{
key: Types.Scalars['String']['input'];
}>;
-export type GetUserMetadataQuery = { user: { __typename: 'UserNode', id: string, bio: string, login: string, name: string } | null };
+export type GetUserMetadataQuery = { user: { __typename: 'UserNode', badgeCount: number, bio: string, id: string, login: string, name: string } | null };
-export const GetLeaderDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLeaderData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
-export const GetUserDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recentIssues"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"closedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentReleases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isPreRelease"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"publishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"tagName"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributedRepositories"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"organization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"subscribersCount"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"contributionsCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"followersCount"}},{"kind":"Field","name":{"kind":"Name","value":"followingCount"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"publicRepositoriesCount"}},{"kind":"Field","name":{"kind":"Name","value":"releasesCount"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode;
-export const GetUserMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
+export const GetLeaderDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLeaderData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}},{"kind":"Field","name":{"kind":"Name","value":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cssClass"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}}]}}]}}]} as unknown as DocumentNode;
+export const GetUserDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recentIssues"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"closedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentReleases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}},{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isPreRelease"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"publishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"tagName"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributedRepositories"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"organization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"subscribersCount"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}},{"kind":"Field","name":{"kind":"Name","value":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cssClass"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"contributionsCount"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"followersCount"}},{"kind":"Field","name":{"kind":"Name","value":"followingCount"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"publicRepositoriesCount"}},{"kind":"Field","name":{"kind":"Name","value":"releasesCount"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode;
+export const GetUserMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"login"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
diff --git a/frontend/src/types/badge.ts b/frontend/src/types/badge.ts
new file mode 100644
index 0000000000..9fb95f689a
--- /dev/null
+++ b/frontend/src/types/badge.ts
@@ -0,0 +1,7 @@
+export type Badge = {
+ readonly cssClass?: string
+ readonly description?: string
+ readonly id: string
+ readonly name: string
+ readonly weight: number
+}
diff --git a/frontend/src/types/card.ts b/frontend/src/types/card.ts
index 46d3d79abe..8e9125989e 100644
--- a/frontend/src/types/card.ts
+++ b/frontend/src/types/card.ts
@@ -1,5 +1,6 @@
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import type { JSX } from 'react'
+import type { Badge } from 'types/badge'
import type { Button } from 'types/button'
import type { Chapter } from 'types/chapter'
import type { Contributor } from 'types/contributor'
@@ -78,6 +79,8 @@ export interface DetailsCardProps {
export interface UserCardProps {
avatar: string
+ badgeCount?: number
+ badges?: Badge[]
button: Button
className?: string
company?: string
diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts
index 349618b99e..293933f89f 100644
--- a/frontend/src/types/user.ts
+++ b/frontend/src/types/user.ts
@@ -1,3 +1,4 @@
+import type { Badge } from 'types/badge'
import type { Issue } from 'types/issue'
import type { RepositoryCardProps } from 'types/project'
import type { Release } from 'types/release'
@@ -9,6 +10,8 @@ export type RepositoryDetails = {
export type User = {
avatarUrl: string
+ badgeCount?: number
+ badges?: Badge[]
bio?: string
company?: string
contributionsCount?: number
diff --git a/frontend/src/utils/data.ts b/frontend/src/utils/data.ts
index 1be07c97ff..837ac1d829 100644
--- a/frontend/src/utils/data.ts
+++ b/frontend/src/utils/data.ts
@@ -1,3 +1,4 @@
+import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faDiscord,
@@ -6,8 +7,8 @@ import {
faLinkedin,
faMeetup,
faSlack,
- faYoutube,
faXTwitter,
+ faYoutube,
} from '@fortawesome/free-brands-svg-icons'
import {
faClock,
@@ -18,6 +19,9 @@ import {
} from '@fortawesome/free-regular-svg-icons'
import {
faArrowsRotate,
+ faAward,
+ faBug,
+ faCertificate,
faCity,
faCode,
faCodeFork,
@@ -25,44 +29,61 @@ import {
faFlag,
faFlask,
faGlobe,
+ faMedal,
faMoon,
+ faPeopleGroup,
+ faRibbon,
faRightToBracket,
+ faStar as faSolidStar,
+ faSun,
faWandMagicSparkles,
faX,
- faPeopleGroup,
- faSun,
} from '@fortawesome/free-solid-svg-icons'
library.add(
faArrowsRotate,
- faCodeFork,
- faStar,
- faUser,
+ faAward,
+ faBug,
+ faCertificate,
+ faCity,
faClock,
+ faCode,
+ faCodeFork,
faComment,
+ faDiscord,
faEgg,
- faFlask,
- faCity,
+ faFacebook,
faFlag,
- faCode,
- faMoon,
- faLightbulb,
- faWandMagicSparkles,
+ faFlask,
faGlobe,
- faRightToBracket,
- faYoutube,
- faX,
faGoogle,
- faMeetup,
+ faLightbulb,
faLinkedin,
- faFacebook,
- faDiscord,
- faSlack,
+ faMedal,
+ faMeetup,
+ faMoon,
faPeopleGroup,
+ faRibbon,
+ faRightToBracket,
+ faSlack,
+ faStar,
+ faSun,
+ faUser,
+ faWandMagicSparkles,
+ faX,
faXTwitter,
- faSun
+ faYoutube
)
+export const BADGE_CLASS_MAP: Record = {
+ award: faAward,
+ bugSlash: faBug,
+ certificate: faCertificate,
+ medal: faMedal,
+ ribbon: faRibbon,
+ star: faSolidStar,
+} as const
+
export const ICONS = {
starsCount: {
label: 'GitHub stars',