From 6f9482ef45346a6b7e97d0dfdc56aa4750e5beaa Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Sat, 6 Sep 2025 19:09:12 +0530 Subject: [PATCH 01/37] created some initial file and add some changes --- frontend/src/components/Badges.tsx | 17 +++++++++++++++++ frontend/src/server/queries/userQueries.ts | 14 ++++++++++++++ frontend/src/types/badge.ts | 8 ++++++++ frontend/src/types/card.ts | 2 ++ frontend/src/types/user.ts | 2 ++ 5 files changed, 43 insertions(+) create mode 100644 frontend/src/components/Badges.tsx create mode 100644 frontend/src/types/badge.ts diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx new file mode 100644 index 0000000000..81321c530a --- /dev/null +++ b/frontend/src/components/Badges.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +type BadgeProps = { + name: string; + imageUrl: string; + description: string; + cssClass: string; +} +const Badges = () => { + return ( +
+ +
+ ) +} + +export default Badges diff --git a/frontend/src/server/queries/userQueries.ts b/frontend/src/server/queries/userQueries.ts index dd9c24cb76..ea9418ef46 100644 --- a/frontend/src/server/queries/userQueries.ts +++ b/frontend/src/server/queries/userQueries.ts @@ -7,6 +7,13 @@ export const GET_LEADER_DATA = gql` avatarUrl login name + badges { + id + name + iconUrl + description + cssClass + } } } ` @@ -78,6 +85,13 @@ export const GET_USER_DATA = gql` location login name + badges { + id + name + iconUrl + description + cssClass + } publicRepositoriesCount releasesCount updatedAt diff --git a/frontend/src/types/badge.ts b/frontend/src/types/badge.ts new file mode 100644 index 0000000000..a204af0571 --- /dev/null +++ b/frontend/src/types/badge.ts @@ -0,0 +1,8 @@ +export type Badge = { + id: string + name: string + iconUrl?: string + cssClass?: string + description?: string + weight: number +} \ No newline at end of file diff --git a/frontend/src/types/card.ts b/frontend/src/types/card.ts index 9f37a9011e..9f09507ff2 100644 --- a/frontend/src/types/card.ts +++ b/frontend/src/types/card.ts @@ -12,6 +12,7 @@ import type { Milestone } from 'types/milestone' import type { RepositoryCardProps } from 'types/project' import type { PullRequest } from 'types/pullRequest' import type { Release } from 'types/release' +import { Badge } from './badge' export type CardProps = { button: Button @@ -76,6 +77,7 @@ export interface DetailsCardProps { export interface UserCardProps { avatar: string + badges?: Badge[] button: Button className?: string company?: string diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts index f2b50891a0..65447d7bff 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' @@ -10,6 +11,7 @@ export type RepositoryDetails = { export type User = { avatarUrl: string bio?: string + badges:Badge[] company?: string contributionsCount: number createdAt: T From 2289fddfe48e49f8816c37035a0536ebe6bc82c1 Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Sat, 13 Sep 2025 22:57:55 +0530 Subject: [PATCH 02/37] Add Badge functionality to UserNode and integrate Badges component - Introduced BadgeNode for GraphQL representation of badges. - Enhanced UserNode with badges and badge count fields. - Updated UserCard to display badge count and integrated Badges component in UserDetailsPage. - Added tests for badge display and functionality across components. - Updated GraphQL queries to include badge data and counts. --- .../apps/github/api/internal/nodes/user.py | 12 + backend/apps/nest/api/internal/nodes/badge.py | 19 + cspell/custom-dict.txt | 1 + .../__tests__/unit/components/Badges.test.tsx | 335 ++++++++++++++++++ .../unit/components/UserCard.test.tsx | 39 ++ frontend/__tests__/unit/data/mockBadgeData.ts | 63 ++++ .../__tests__/unit/data/mockUserDetails.ts | 17 + .../__tests__/unit/pages/UserDetails.test.tsx | 257 +++++++++++++- frontend/src/app/members/[memberKey]/page.tsx | 16 + frontend/src/app/members/page.tsx | 4 + frontend/src/components/Badges.tsx | 52 ++- frontend/src/components/UserCard.tsx | 15 +- frontend/src/server/queries/userQueries.ts | 11 +- frontend/src/types/badge.ts | 13 +- frontend/src/types/card.ts | 3 +- frontend/src/types/user.ts | 3 +- 16 files changed, 837 insertions(+), 23 deletions(-) create mode 100644 backend/apps/nest/api/internal/nodes/badge.py create mode 100644 frontend/__tests__/unit/components/Badges.test.tsx create mode 100644 frontend/__tests__/unit/data/mockBadgeData.ts diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index edec212597..6ee1f9c550 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -4,6 +4,7 @@ import strawberry_django from apps.github.models.user import User +from apps.nest.api.internal.nodes.badge import BadgeNode @strawberry_django.type( @@ -27,6 +28,17 @@ class UserNode: """GitHub user node.""" + @strawberry.field + def badges(self) -> list[BadgeNode]: + """List badges assigned to the user sorted by weight and name.""" + user_badges = self.badges.filter(is_active=True).select_related("badge") + return [user_badge.badge for user_badge in user_badges] + + @strawberry.field + def badge_count(self) -> int: + """Resolve badge count.""" + return self.badges.filter(is_active=True).count() + @strawberry.field def created_at(self) -> float: """Resolve created at.""" diff --git a/backend/apps/nest/api/internal/nodes/badge.py b/backend/apps/nest/api/internal/nodes/badge.py new file mode 100644 index 0000000000..1fd5ba9375 --- /dev/null +++ b/backend/apps/nest/api/internal/nodes/badge.py @@ -0,0 +1,19 @@ +"""GraphQL node for Badge model.""" + +import strawberry +import strawberry_django +from apps.nest.models import Badge + + +@strawberry_django.type( + Badge, + fields=[ + "id", + "name", + "description", + "weight", + "css_class", + ], +) +class BadgeNode: + """GraphQL node for Badge model.""" diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index fff4dc5fa4..a0f8e2f93d 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -56,6 +56,7 @@ dismissable dsn env facebookexternalhit +fas gamesec geocoders geoloc diff --git a/frontend/__tests__/unit/components/Badges.test.tsx b/frontend/__tests__/unit/components/Badges.test.tsx new file mode 100644 index 0000000000..ccdfff6cfa --- /dev/null +++ b/frontend/__tests__/unit/components/Badges.test.tsx @@ -0,0 +1,335 @@ +import { render, screen, cleanup } from '@testing-library/react' +import React from 'react' +import Badges from 'components/Badges' + +// Mock FontAwesome components +jest.mock('@fortawesome/react-fontawesome', () => ({ + FontAwesomeIcon: ({ + icon, + className, + ...props + }: { + icon: string[] | { iconName: string } + className?: string + [key: string]: unknown + }) => { + const iconName = Array.isArray(icon) ? icon[1] : icon.iconName + return + }, +})) + +// Mock FontAwesome library +jest.mock('@fortawesome/fontawesome-svg-core', () => ({ + library: { + add: jest.fn(), + }, + ICON_NAME: {} as unknown, +})) + +// Mock FontAwesome icons +jest.mock('@fortawesome/free-solid-svg-icons', () => ({ + fas: { + medal: { iconName: 'medal' }, + 'shield-alt': { iconName: 'shield-alt' }, + code: { iconName: 'code' }, + 'user-graduate': { iconName: 'user-graduate' }, + crown: { iconName: 'crown' }, + star: { iconName: 'star' }, + trophy: { iconName: 'trophy' }, + 'user-tie': { iconName: 'user-tie' }, + }, +})) + +// Mock Tooltip component +jest.mock('@heroui/tooltip', () => { + const MockTooltip = ({ + children, + content, + ...props + }: { + children: React.ReactNode + content: string + [key: string]: unknown + }) => ( +
+ {children} +
+ ) + MockTooltip.displayName = 'MockTooltip' + return { + __esModule: true, + Tooltip: MockTooltip, + } +}) + +describe('Badges', () => { + const defaultProps = { + name: 'Test Badge', + weight: 1, + description: 'Test description', + cssClass: 'fa-medal', + } + + afterEach(() => { + cleanup() + }) + + describe('Essential Rendering Tests', () => { + it('renders successfully with valid icon', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') + }) + + it('renders without tooltip when showTooltip is false', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument() + }) + + it('renders with tooltip by default when showTooltip is not specified', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toBeInTheDocument() + }) + }) + + describe('Icon Handling', () => { + it('renders correct icon for valid cssClass', () => { + render() + + expect(screen.getByTestId('icon-crown')).toBeInTheDocument() + }) + + it('renders correct icon for cssClass without fa- prefix', () => { + render() + + expect(screen.getByTestId('icon-crown')).toBeInTheDocument() + }) + + it('renders correct icon for cssClass with multiple fa- prefixes', () => { + render() + + // Should fall back to medal icon since 'fa-crown' is not a valid FontAwesome icon + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + }) + + it('handles different valid icons', () => { + const icons = ['fa-medal', 'fa-crown', 'fa-star', 'fa-trophy'] + icons.forEach((icon) => { + const { unmount } = render() + expect(screen.getByTestId(`icon-${icon.replace('fa-', '')}`)).toBeInTheDocument() + unmount() + }) + }) + }) + + describe('Fallback Behavior', () => { + it('renders fallback medal icon for invalid cssClass', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute( + 'data-content', + 'Test Badge (icon not found)' + ) + }) + + it('shows fallback tooltip content for invalid icon', () => { + render() + + expect(screen.getByTestId('tooltip')).toHaveAttribute( + 'data-content', + 'Test Badge (icon not found)' + ) + }) + + it('renders fallback icon without tooltip when showTooltip is false', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument() + }) + + it('handles empty cssClass gracefully', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') + }) + + it('handles undefined cssClass gracefully', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') + }) + }) + + describe('Tooltip Content', () => { + it('displays badge name in tooltip for valid icon', () => { + render() + + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Special Badge') + }) + + it('displays modified tooltip content for invalid icon', () => { + render() + + expect(screen.getByTestId('tooltip')).toHaveAttribute( + 'data-content', + 'Invalid Badge (icon not found)' + ) + }) + + it('handles special characters in badge name', () => { + render() + + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Badge & More!') + }) + + it('handles long badge names', () => { + const longName = 'Very Long Badge Name That Exceeds Normal Length' + render() + + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', longName) + }) + }) + + describe('CSS Class Processing', () => { + it('correctly processes cssClass with fa- prefix', () => { + render() + + expect(screen.getByTestId('icon-crown')).toBeInTheDocument() + }) + + it('correctly processes cssClass without fa- prefix', () => { + render() + + expect(screen.getByTestId('icon-crown')).toBeInTheDocument() + }) + + it('handles cssClass with multiple dashes', () => { + render() + + expect(screen.getByTestId('icon-user-tie')).toBeInTheDocument() + }) + + it('handles cssClass with underscores', () => { + render() + + // The component should fall back to medal icon since 'user_tie' is not a valid FontAwesome icon + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + }) + }) + + describe('Styling and Classes', () => { + it('applies correct classes to valid icon', () => { + render() + + const icon = screen.getByTestId('icon-medal') + expect(icon).toHaveClass('h-4', 'w-4') + }) + + it('applies correct classes to fallback icon', () => { + render() + + const icon = screen.getByTestId('icon-medal') + expect(icon).toHaveClass('h-4', 'w-4', 'text-gray-400') + }) + + it('wraps icon in inline-flex container', () => { + render() + + const container = screen.getByTestId('icon-medal').parentElement?.parentElement + expect(container).toHaveClass('inline-flex', 'items-center') + }) + }) + + describe('Edge Cases and Error Handling', () => { + it('handles null cssClass', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') + }) + + it('handles numeric cssClass', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute( + 'data-content', + 'Test Badge (icon not found)' + ) + }) + + it('handles boolean cssClass', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute( + 'data-content', + 'Test Badge (icon not found)' + ) + }) + + it('handles whitespace-only cssClass', () => { + render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute( + 'data-content', + 'Test Badge (icon not found)' + ) + }) + }) + + describe('Accessibility', () => { + it('provides tooltip content for screen readers', () => { + render() + + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Accessible Badge') + }) + + it('maintains tooltip accessibility for fallback icons', () => { + render() + + expect(screen.getByTestId('tooltip')).toHaveAttribute( + 'data-content', + 'Fallback Badge (icon not found)' + ) + }) + }) + + describe('Performance and Optimization', () => { + it('renders efficiently with multiple badges', () => { + const badges = [ + { ...defaultProps, name: 'Badge 1', cssClass: 'fa-medal' }, + { ...defaultProps, name: 'Badge 2', cssClass: 'fa-crown' }, + { ...defaultProps, name: 'Badge 3', cssClass: 'fa-star' }, + ] + + badges.forEach((badge, _index) => { + const { unmount } = render() + expect(screen.getByTestId(`icon-${badge.cssClass.replace('fa-', '')}`)).toBeInTheDocument() + unmount() + }) + }) + + it('handles rapid re-renders with different props', () => { + const { rerender } = render() + + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + + rerender() + expect(screen.getByTestId('icon-crown')).toBeInTheDocument() + + rerender() + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + }) + }) +}) 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..5f1f1cfe7d --- /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 75bdf32930..d04f9f9a9c 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', () => ({ // 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(), } @@ -478,4 +515,222 @@ 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 jest.Mock).mockReturnValue({ + data: mockUserDetailsData, + loading: false, + error: null, + }) + + render() + await waitFor(() => { + expect(screen.getByText('Badges:')).toBeInTheDocument() + expect(screen.getByTestId('badge-contributor')).toBeInTheDocument() + expect(screen.getByTestId('badge-security-expert')).toBeInTheDocument() + }) + }) + + test('renders badges with correct props', async () => { + ;(useQuery 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 jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) + + render() + await waitFor(() => { + expect(screen.queryByText('Badges:')).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 jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) + + render() + await waitFor(() => { + expect(screen.queryByText('Badges:')).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 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 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('renders multiple badges in correct order', async () => { + ;(useQuery as jest.Mock).mockReturnValue({ + data: mockUserDetailsData, + loading: false, + error: null, + }) + + render() + await waitFor(() => { + const badgesContainer = screen.getByText('Badges:').parentElement + const badges = badgesContainer?.querySelectorAll('[data-testid^="badge-"]') + + expect(badges).toHaveLength(2) + expect(badges?.[0]).toHaveAttribute('data-testid', 'badge-contributor') + expect(badges?.[1]).toHaveAttribute('data-testid', 'badge-security-expert') + }) + }) + + 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 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 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() + }) + }) + }) }) diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 3ea4f24d54..8af3485aa4 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -21,6 +21,7 @@ import type { Release } from 'types/release' import type { UserDetails } 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' @@ -208,6 +209,21 @@ const UserDetailsPage: React.FC = () => { @{user?.login}

{formattedBio}

+ {user?.badges && user.badges.length > 0 && ( +
+ Badges: + {user.badges.map((badge) => ( + + ))} +
+ )} ) diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx index 955210d6d6..477d1559be 100644 --- a/frontend/src/app/members/page.tsx +++ b/frontend/src/app/members/page.tsx @@ -34,11 +34,15 @@ const UsersPage = () => { onclick: () => handleButtonClick(user), } + const badgeCount = user.badges?.length || 0 + return ( { + +const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { + // Extract icon name from cssClass (remove 'fa-' prefix) + // Handle edge cases where cssClass might be null, undefined, or not a string + const safeCssClass = cssClass || 'fa-medal' + const iconName = String(safeCssClass).replace(/^fa-/, '') as IconName + + // Check if the icon exists in FontAwesome + const iconExists = fas[iconName] + + if (!iconExists) { + // Fallback to a default icon if the specified icon doesn't exist + return ( +
+ {showTooltip ? ( + + + + ) : ( + + )} +
+ ) + } + return ( -
- +
+ {showTooltip ? ( + + + + ) : ( + + )}
) } diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx index 4df1aec7b6..1a8afeac91 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' @@ -8,6 +14,7 @@ import type { UserCardProps } from 'types/card' const UserCard = ({ avatar, button, + badgeCount, className, company, description, @@ -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 ea9418ef46..8c14d65137 100644 --- a/frontend/src/server/queries/userQueries.ts +++ b/frontend/src/server/queries/userQueries.ts @@ -10,10 +10,11 @@ export const GET_LEADER_DATA = gql` badges { id name - iconUrl description cssClass + weight } + badgeCount } } ` @@ -86,12 +87,13 @@ export const GET_USER_DATA = gql` login name badges { + cssClass + description id name - iconUrl - description - cssClass + weight } + badgeCount publicRepositoriesCount releasesCount updatedAt @@ -106,6 +108,7 @@ export const GET_USER_METADATA = gql` bio login name + badgeCount } } ` diff --git a/frontend/src/types/badge.ts b/frontend/src/types/badge.ts index a204af0571..4dbc73224b 100644 --- a/frontend/src/types/badge.ts +++ b/frontend/src/types/badge.ts @@ -1,8 +1,7 @@ export type Badge = { - id: string - name: string - iconUrl?: string - cssClass?: string - description?: string - weight: number -} \ No newline at end of file + id: string + name: string + cssClass?: string + description?: string + weight: number +} diff --git a/frontend/src/types/card.ts b/frontend/src/types/card.ts index 9f09507ff2..4e8923e0b8 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' @@ -12,7 +13,6 @@ import type { Milestone } from 'types/milestone' import type { RepositoryCardProps } from 'types/project' import type { PullRequest } from 'types/pullRequest' import type { Release } from 'types/release' -import { Badge } from './badge' export type CardProps = { button: Button @@ -78,6 +78,7 @@ export interface DetailsCardProps { export interface UserCardProps { avatar: string badges?: Badge[] + badgeCount?: number button: Button className?: string company?: string diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts index 65447d7bff..965accbc9c 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -11,7 +11,8 @@ export type RepositoryDetails = { export type User = { avatarUrl: string bio?: string - badges:Badge[] + badges?: Badge[] + badgeCount?: number company?: string contributionsCount: number createdAt: T From 5b947da506f422df6053ba117b690e4ea2806112 Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Sat, 13 Sep 2025 23:16:24 +0530 Subject: [PATCH 03/37] updated the backend file for sorting badges --- .../apps/github/api/internal/nodes/user.py | 6 +- backend/apps/nest/api/internal/nodes/badge.py | 2 +- .../github/api/internal/nodes/user_test.py | 114 ++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index 6ee1f9c550..112b602053 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -31,7 +31,11 @@ class UserNode: @strawberry.field def badges(self) -> list[BadgeNode]: """List badges assigned to the user sorted by weight and name.""" - user_badges = self.badges.filter(is_active=True).select_related("badge") + user_badges = ( + self.badges.filter(is_active=True) + .select_related("badge") + .order_by("-badge__weight", "badge__name") + ) return [user_badge.badge for user_badge in user_badges] @strawberry.field diff --git a/backend/apps/nest/api/internal/nodes/badge.py b/backend/apps/nest/api/internal/nodes/badge.py index 1fd5ba9375..0c6fdcbd47 100644 --- a/backend/apps/nest/api/internal/nodes/badge.py +++ b/backend/apps/nest/api/internal/nodes/badge.py @@ -1,7 +1,7 @@ """GraphQL node for Badge model.""" -import strawberry import strawberry_django + from apps.nest.models import Badge 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 01237ad925..0135282a8a 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,8 @@ 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", "contributions_count", @@ -78,3 +81,114 @@ def test_url_field(self): result = UserNode.url(mock_user) assert result == "https://github.com/testuser" + + 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.badges = mock_badges_queryset + + result = UserNode.badge_count(mock_user) + assert result == 3 + mock_badges_queryset.filter.assert_called_once_with(is_active=True) + + def test_badges_field_empty(self): + """Test badges field resolution with no badges.""" + mock_user = Mock() + mock_badges_queryset = Mock() + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value = [] + mock_user.badges = mock_badges_queryset + + result = UserNode.badges(mock_user) + assert result == [] + + 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 + + mock_badges_queryset = Mock() + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value = [ + mock_user_badge + ] + mock_user.badges = mock_badges_queryset + + result = UserNode.badges(mock_user) + assert result == [mock_badge] + + 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 + # (highest weight first, then by name for same weight) + mock_badges_queryset = Mock() + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value = [ + mock_user_badge_high, # weight 100 + 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_low, # weight 10 + ] + mock_user = Mock() + mock_user.badges = mock_badges_queryset + + result = UserNode.badges(mock_user) + + # Verify the badges are returned in the correct order + expected_badges = [ + mock_badge_high_weight, + mock_badge_medium_weight_a, + mock_badge_medium_weight_b, + mock_badge_low_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_badges_queryset.filter.return_value.select_related.assert_called_once_with("badge") + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.assert_called_once_with( + "-badge__weight", "badge__name" + ) + + def test_badges_field_filters_inactive_badges(self): + """Test badges field only returns active badges.""" + mock_user = Mock() + mock_badges_queryset = Mock() + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value = [] + mock_user.badges = mock_badges_queryset + + UserNode.badges(mock_user) + + # Verify that only active badges are filtered + mock_badges_queryset.filter.assert_called_once_with(is_active=True) From f22d2e035c3e016d5dceecbccdea283c592eb6a1 Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Sat, 13 Sep 2025 23:41:02 +0530 Subject: [PATCH 04/37] Refactor tests and mock data for badges functionality - Improved formatting and readability in user_test.py by adjusting line breaks. - Updated mockBadgeData.ts to enhance consistency in object formatting. - Simplified Tooltip mock in Badges.test.tsx for better clarity. - Minor adjustments in Badges.tsx for consistent className formatting. --- .../github/api/internal/nodes/user_test.py | 18 ++-- .../__tests__/unit/components/Badges.test.tsx | 14 +-- frontend/__tests__/unit/data/mockBadgeData.ts | 100 +++++++++--------- frontend/src/components/Badges.tsx | 2 +- 4 files changed, 67 insertions(+), 67 deletions(-) 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 0135282a8a..903a2c747b 100644 --- a/backend/tests/apps/github/api/internal/nodes/user_test.py +++ b/backend/tests/apps/github/api/internal/nodes/user_test.py @@ -97,7 +97,9 @@ def test_badges_field_empty(self): """Test badges field resolution with no badges.""" mock_user = Mock() mock_badges_queryset = Mock() - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value = [] + ( + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value + ) = [] mock_user.badges = mock_badges_queryset result = UserNode.badges(mock_user) @@ -111,9 +113,9 @@ def test_badges_field_single_badge(self): mock_user_badge.badge = mock_badge mock_badges_queryset = Mock() - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value = [ - mock_user_badge - ] + ( + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value + ) = [mock_user_badge] mock_user.badges = mock_badges_queryset result = UserNode.badges(mock_user) @@ -154,7 +156,9 @@ def test_badges_field_sorted_by_weight_and_name(self): # Set up the mock queryset to return badges in the expected sorted order # (highest weight first, then by name for same weight) mock_badges_queryset = Mock() - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value = [ + ( + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value + ) = [ mock_user_badge_high, # weight 100 mock_user_badge_medium_a, # weight 50, name "Medium Weight A" mock_user_badge_medium_b, # weight 50, name "Medium Weight B" @@ -185,7 +189,9 @@ def test_badges_field_filters_inactive_badges(self): """Test badges field only returns active badges.""" mock_user = Mock() mock_badges_queryset = Mock() - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value = [] + ( + mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value + ) = [] mock_user.badges = mock_badges_queryset UserNode.badges(mock_user) diff --git a/frontend/__tests__/unit/components/Badges.test.tsx b/frontend/__tests__/unit/components/Badges.test.tsx index ccdfff6cfa..377ec09799 100644 --- a/frontend/__tests__/unit/components/Badges.test.tsx +++ b/frontend/__tests__/unit/components/Badges.test.tsx @@ -40,9 +40,8 @@ jest.mock('@fortawesome/free-solid-svg-icons', () => ({ }, })) -// Mock Tooltip component -jest.mock('@heroui/tooltip', () => { - const MockTooltip = ({ +jest.mock('@heroui/tooltip', () => ({ + Tooltip: ({ children, content, ...props @@ -54,13 +53,8 @@ jest.mock('@heroui/tooltip', () => {
{children}
- ) - MockTooltip.displayName = 'MockTooltip' - return { - __esModule: true, - Tooltip: MockTooltip, - } -}) + ), +})) describe('Badges', () => { const defaultProps = { diff --git a/frontend/__tests__/unit/data/mockBadgeData.ts b/frontend/__tests__/unit/data/mockBadgeData.ts index 5f1f1cfe7d..afde137fd2 100644 --- a/frontend/__tests__/unit/data/mockBadgeData.ts +++ b/frontend/__tests__/unit/data/mockBadgeData.ts @@ -1,63 +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, - }, + { + 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, - }, + 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, - }, + user: { + id: '2', + login: 'testuser2', + name: 'Test User 2', + badges: [], + badgeCount: 0, + }, } export const mockBadgeQueryResponse = { - badges: mockBadgeData, + badges: mockBadgeData, } diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 99e6a8ed44..e75f5b1c3e 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -27,7 +27,7 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { return (
{showTooltip ? ( - + ) : ( From e78e42ab06cddb1a6b74be97d544cd0416bc5c3c Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:34:46 +0530 Subject: [PATCH 05/37] Update frontend/__tests__/unit/components/Badges.test.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../__tests__/unit/components/Badges.test.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/frontend/__tests__/unit/components/Badges.test.tsx b/frontend/__tests__/unit/components/Badges.test.tsx index 377ec09799..39a66d91f4 100644 --- a/frontend/__tests__/unit/components/Badges.test.tsx +++ b/frontend/__tests__/unit/components/Badges.test.tsx @@ -20,10 +20,22 @@ jest.mock('@fortawesome/react-fontawesome', () => ({ // Mock FontAwesome library jest.mock('@fortawesome/fontawesome-svg-core', () => ({ - library: { - add: jest.fn(), - }, - ICON_NAME: {} as unknown, + library: { add: jest.fn() }, + // Simulate FA lookup; throw if not found + findIconDefinition: jest.fn(({ iconName }: { iconName: string }) => { + const valid = new Set([ + 'medal', + 'shield-alt', + 'code', + 'user-graduate', + 'crown', + 'star', + 'trophy', + 'user-tie', + ]) + if (valid.has(iconName)) return { iconName } + throw new Error('not found') + }), })) // Mock FontAwesome icons From dcb90b70b9a0ffb3e306b2117cf4a636ae52e952 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:35:06 +0530 Subject: [PATCH 06/37] Update frontend/src/components/Badges.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- frontend/src/components/Badges.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index e75f5b1c3e..718979b2d5 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -19,10 +19,23 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { const safeCssClass = cssClass || 'fa-medal' const iconName = String(safeCssClass).replace(/^fa-/, '') as IconName - // Check if the icon exists in FontAwesome - const iconExists = fas[iconName] +import { IconName, IconLookup, findIconDefinition, library } from '@fortawesome/fontawesome-svg-core' - if (!iconExists) { + const safeCssClass = (cssClass ?? 'fa-medal') as string + // Remove one or more 'fa-' prefixes, normalize underscores to dashes + const iconName = String(safeCssClass).trim().replace(/^(?:fa-)+/, '').replace(/_/g, '-') as IconName + + // Check if the icon exists in the FA library + const lookup: IconLookup = { prefix: 'fas', iconName } + let iconFound = false + try { + findIconDefinition(lookup) + iconFound = true + } catch { + iconFound = false + } + + if (!iconFound) { // Fallback to a default icon if the specified icon doesn't exist return (
From 7f270cf3d388351cfd010b05c3593dd0d12b3391 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:35:24 +0530 Subject: [PATCH 07/37] Update frontend/__tests__/unit/pages/UserDetails.test.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- frontend/__tests__/unit/pages/UserDetails.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index d04f9f9a9c..db9c56392d 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -668,8 +668,8 @@ describe('UserDetailsPage', () => { const badges = badgesContainer?.querySelectorAll('[data-testid^="badge-"]') expect(badges).toHaveLength(2) - expect(badges?.[0]).toHaveAttribute('data-testid', 'badge-contributor') - expect(badges?.[1]).toHaveAttribute('data-testid', 'badge-security-expert') + expect(badges?.[0]).toHaveAttribute('data-testid', 'badge-security-expert') + expect(badges?.[1]).toHaveAttribute('data-testid', 'badge-contributor') }) }) From 4fdca0f3a32268e931535dadd31c6d6d9efbf25d Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:37:19 +0530 Subject: [PATCH 08/37] Add Font Awesome icons to custom dictionary --- cspell/custom-dict.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index a0f8e2f93d..498bd930fb 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -57,6 +57,11 @@ dsn env facebookexternalhit fas +far +fab +fa-solid +fa-regular +fa-brands gamesec geocoders geoloc From e402712d76044247b35a26b2de1dfbe38e889b7d Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:38:40 +0530 Subject: [PATCH 09/37] Make Badge properties readonly --- frontend/src/types/badge.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/types/badge.ts b/frontend/src/types/badge.ts index 4dbc73224b..ca50a8872f 100644 --- a/frontend/src/types/badge.ts +++ b/frontend/src/types/badge.ts @@ -1,7 +1,7 @@ export type Badge = { - id: string - name: string - cssClass?: string - description?: string - weight: number + readonly id: string + readonly name: string + readonly cssClass?: string + readonly description?: string + readonly weight: number } From 21d68033c1bf107abc712a731f144aad5677264f Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:41:36 +0530 Subject: [PATCH 10/37] Sort badges by weight before rendering --- frontend/src/app/members/[memberKey]/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 8af3485aa4..ce36d3687e 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -212,7 +212,10 @@ const UserDetailsPage: React.FC = () => { {user?.badges && user.badges.length > 0 && (
Badges: - {user.badges.map((badge) => ( + {user.badges + .slice() + .sort((a,b) => (b.weight - a.weight) + .map((badge) => ( Date: Sun, 14 Sep 2025 13:42:27 +0530 Subject: [PATCH 11/37] Use nullish coalescing for user badges --- frontend/src/app/members/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx index 477d1559be..93a730889d 100644 --- a/frontend/src/app/members/page.tsx +++ b/frontend/src/app/members/page.tsx @@ -41,7 +41,7 @@ const UsersPage = () => { avatar={user.avatarUrl} button={submitButton} className="h-64 w-80 bg-white p-6 text-left shadow-lg transition-transform duration-500 hover:scale-105 hover:shadow-xl dark:bg-gray-800 dark:shadow-gray-900/30" - badges={user.badges || []} + badges={user.badges ?? []} badgeCount={badgeCount} company={user.company || ''} email={user.email || ''} From a38e89dfc028fd53f9e7a2e840c31c015a23db18 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:44:16 +0530 Subject: [PATCH 12/37] Add aria-label to FontAwesomeIcon for accessibility --- frontend/src/components/UserCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx index 1a8afeac91..348d30bb16 100644 --- a/frontend/src/components/UserCard.tsx +++ b/frontend/src/components/UserCard.tsx @@ -71,7 +71,7 @@ const UserCard = ({ )} {badgeCount > 0 && (

- + {millify(badgeCount, { precision: 1 })}{' '}

)} From 984da0d92c497711fcdb638a5e4f85348e52d7c5 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:46:58 +0530 Subject: [PATCH 13/37] Update user.py --- backend/apps/github/api/internal/nodes/user.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index 112b602053..3bff082f59 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -41,7 +41,12 @@ def badges(self) -> list[BadgeNode]: @strawberry.field def badge_count(self) -> int: """Resolve badge count.""" - return self.badges.filter(is_active=True).count() + return ( + self.badges.filter(is_active=True) + .value("badge_id") + .distinct() + .count() + ) @strawberry.field def created_at(self) -> float: From 39a7a8c6b406252da55f95d0d6efb1b28be6c525 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:12:56 +0000 Subject: [PATCH 14/37] Refactor badge count resolver for improved readability and performance; update badge component imports and cleanup --- .../apps/github/api/internal/nodes/user.py | 7 +---- backend/apps/nest/api/internal/nodes/badge.py | 3 ++- cspell/custom-dict.txt | 10 +++---- frontend/src/app/members/[memberKey]/page.tsx | 26 +++++++++---------- frontend/src/components/Badges.tsx | 21 +++++++-------- 5 files changed, 30 insertions(+), 37 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index 3bff082f59..c9e2f14a2b 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -41,12 +41,7 @@ def badges(self) -> list[BadgeNode]: @strawberry.field def badge_count(self) -> int: """Resolve badge count.""" - return ( - self.badges.filter(is_active=True) - .value("badge_id") - .distinct() - .count() - ) + return self.badges.filter(is_active=True).value("badge_id").distinct().count() @strawberry.field def created_at(self) -> float: diff --git a/backend/apps/nest/api/internal/nodes/badge.py b/backend/apps/nest/api/internal/nodes/badge.py index 443e3b2bc9..0c6fdcbd47 100644 --- a/backend/apps/nest/api/internal/nodes/badge.py +++ b/backend/apps/nest/api/internal/nodes/badge.py @@ -1,7 +1,8 @@ """GraphQL node for Badge model.""" + import strawberry_django -from apps.nest.models import Badge +from apps.nest.models import Badge @strawberry_django.type( diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index 814a531ab9..3704c8de2d 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -56,13 +56,13 @@ demojize dismissable dsn env +fa-brands +fa-regular +fa-solid +fab facebookexternalhit -fas far -fab -fa-solid -fa-regular -fa-brands +fas gamesec geocoders geoloc diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 499869a3ce..2f1f5ac732 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -204,23 +204,21 @@ const UserDetailsPage: React.FC = () => { Badges: {user.badges .slice() - .sort((a,b) => (b.weight - a.weight) - .map((badge) => ( - - ))} - {!isPrivateContributor && ( + .sort((a, b) => b.weight - a.weight) + .map((badge) => ( + + ))} +
+ )} + {!isPrivateContributor && (
- )} -
)}
diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 718979b2d5..e65b4bdcfa 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -1,4 +1,9 @@ -import { IconName, library } from '@fortawesome/fontawesome-svg-core' +import { + IconName, + IconLookup, + findIconDefinition, + library, +} from '@fortawesome/fontawesome-svg-core' import { fas } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Tooltip } from '@heroui/tooltip' @@ -7,23 +12,17 @@ library.add(fas) type BadgeProps = { name: string - weight: number - description: string cssClass: string showTooltip?: boolean } const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { - // Extract icon name from cssClass (remove 'fa-' prefix) - // Handle edge cases where cssClass might be null, undefined, or not a string - const safeCssClass = cssClass || 'fa-medal' - const iconName = String(safeCssClass).replace(/^fa-/, '') as IconName - -import { IconName, IconLookup, findIconDefinition, library } from '@fortawesome/fontawesome-svg-core' - const safeCssClass = (cssClass ?? 'fa-medal') as string // Remove one or more 'fa-' prefixes, normalize underscores to dashes - const iconName = String(safeCssClass).trim().replace(/^(?:fa-)+/, '').replace(/_/g, '-') as IconName + const iconName = String(safeCssClass) + .trim() + .replace(/^(?:fa-)+/, '') + .replace(/_/g, '-') as IconName // Check if the icon exists in the FA library const lookup: IconLookup = { prefix: 'fas', iconName } From 8e76f8f68e06c15984edee76567fa89b02ca64de Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:31:53 +0530 Subject: [PATCH 15/37] Update backend/apps/github/api/internal/nodes/user.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- backend/apps/github/api/internal/nodes/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index c9e2f14a2b..8b6309efbf 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -41,7 +41,7 @@ def badges(self) -> list[BadgeNode]: @strawberry.field def badge_count(self) -> int: """Resolve badge count.""" - return self.badges.filter(is_active=True).value("badge_id").distinct().count() + return self.badges.filter(is_active=True).values("badge_id").distinct().count() @strawberry.field def created_at(self) -> float: From 81b21d940887967c67eec5a21e15b586c1124e2b Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:32:23 +0530 Subject: [PATCH 16/37] Update frontend/src/components/Badges.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- frontend/src/components/Badges.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index e65b4bdcfa..b95150fcb5 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -5,11 +5,12 @@ import { library, } from '@fortawesome/fontawesome-svg-core' import { fas } from '@fortawesome/free-solid-svg-icons' +import { far } from '@fortawesome/free-regular-svg-icons' +import { fab } from '@fortawesome/free-brands-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Tooltip } from '@heroui/tooltip' -library.add(fas) - +library.add(fas, far, fab) type BadgeProps = { name: string cssClass: string From 8e846f3e40c70550fd46f7a807dc3a80bc39452f Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:33:07 +0530 Subject: [PATCH 17/37] Update frontend/src/components/Badges.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- frontend/src/components/Badges.tsx | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index b95150fcb5..07ff36aaeb 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -18,15 +18,24 @@ type BadgeProps = { } const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { - const safeCssClass = (cssClass ?? 'fa-medal') as string - // Remove one or more 'fa-' prefixes, normalize underscores to dashes - const iconName = String(safeCssClass) - .trim() - .replace(/^(?:fa-)+/, '') - .replace(/_/g, '-') as IconName + const safeCssClass = String(cssClass ?? 'fa-medal').trim() + const tokens = safeCssClass.split(/\s+/) + const styleToken = tokens.find((t) => /^fa-(solid|regular|brands)$/i.test(t)) + const stylePrefix: IconLookup['prefix'] = + styleToken?.toLowerCase() === 'fa-regular' + ? 'far' + : styleToken?.toLowerCase() === 'fa-brands' + ? 'fab' + : 'fas' + const iconToken = + tokens + .slice() + .reverse() + .find((t) => /^fa-[a-z0-9-]+$/i.test(t) && !/^fa-(solid|regular|brands)$/i.test(t)) || 'fa-medal' + const iconName = iconToken.replace(/^fa-/, '').replace(/_/g, '-') as IconName - // Check if the icon exists in the FA library - const lookup: IconLookup = { prefix: 'fas', iconName } + // Check if the icon exists in the FA library for the derived style + const lookup: IconLookup = { prefix: stylePrefix, iconName } let iconFound = false try { findIconDefinition(lookup) From 3cb978594977e263539c377edeaa0495ead28ffb Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Tue, 16 Sep 2025 23:53:56 +0530 Subject: [PATCH 18/37] Add badgeCount field to UserNode and update related queries; refactor badge count test for clarity --- .../apps/github/api/internal/nodes/user_test.py | 6 +++++- frontend/src/components/Badges.tsx | 7 ++++--- frontend/src/types/__generated__/graphql.ts | 3 ++- .../src/types/__generated__/userQueries.generated.ts | 12 ++++++------ 4 files changed, 17 insertions(+), 11 deletions(-) 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 00d02c84e0..0c417ac39e 100644 --- a/backend/tests/apps/github/api/internal/nodes/user_test.py +++ b/backend/tests/apps/github/api/internal/nodes/user_test.py @@ -87,12 +87,16 @@ 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_badges_queryset.filter.return_value.values.return_value.distinct.return_value.count.return_value + ) = 3 mock_user.badges = mock_badges_queryset 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.values.assert_called_once_with("badge_id") + mock_badges_queryset.filter.return_value.values.return_value.distinct.assert_called_once() def test_badges_field_empty(self): """Test badges field resolution with no badges.""" diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 07ff36aaeb..58be15a1f7 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -4,9 +4,9 @@ import { findIconDefinition, library, } from '@fortawesome/fontawesome-svg-core' -import { fas } from '@fortawesome/free-solid-svg-icons' -import { far } from '@fortawesome/free-regular-svg-icons' import { fab } from '@fortawesome/free-brands-svg-icons' +import { far } from '@fortawesome/free-regular-svg-icons' +import { fas } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Tooltip } from '@heroui/tooltip' @@ -31,7 +31,8 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { tokens .slice() .reverse() - .find((t) => /^fa-[a-z0-9-]+$/i.test(t) && !/^fa-(solid|regular|brands)$/i.test(t)) || 'fa-medal' + .find((t) => /^fa-[a-z0-9-]+$/i.test(t) && !/^fa-(solid|regular|brands)$/i.test(t)) || + 'fa-medal' const iconName = iconToken.replace(/^fa-/, '').replace(/_/g, '-') as IconName // Check if the icon exists in the FA library for the derived style diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts index 17f2ff0c06..6bf165686f 100644 --- a/frontend/src/types/__generated__/graphql.ts +++ b/frontend/src/types/__generated__/graphql.ts @@ -37,7 +37,7 @@ export type AuthUserNode = Node & { username: Scalars['String']['output']; }; -export type BadgeNode = Node & { +export type BadgeNode = { __typename?: 'BadgeNode'; cssClass: Scalars['String']['output']; description: Scalars['String']['output']; @@ -859,6 +859,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 5455b2eae6..2cdf040c3a 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', id: string, name: string, description: string, cssClass: string, weight: number }> } | null }; export type GetUserDataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; }>; -export type GetUserDataQuery = { recentIssues: Array<{ __typename: 'IssueNode', id: string, createdAt: unknown, 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: unknown, url: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: unknown, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, publishedAt: unknown | 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: unknown, 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: unknown, url: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: unknown, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, publishedAt: unknown | 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, badgeCount: number, 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', id: string, bio: string, login: string, name: string, badgeCount: number } | 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":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cssClass"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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":"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":"badgeCount"}},{"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"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file From 1121d26bb1eda47957c5fd61651cce76660f533d Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Wed, 17 Sep 2025 00:33:44 +0530 Subject: [PATCH 19/37] frontend: fix Badges non-prefixed and invalid cssClass handling; adjust UserDetails avatar/layout classes to satisfy tests --- .../__tests__/unit/components/Badges.test.tsx | 19 +---- frontend/src/app/members/[memberKey]/page.tsx | 10 +-- frontend/src/components/Badges.tsx | 82 ++++++++++++++++--- 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/frontend/__tests__/unit/components/Badges.test.tsx b/frontend/__tests__/unit/components/Badges.test.tsx index 39a66d91f4..ae9973a2c2 100644 --- a/frontend/__tests__/unit/components/Badges.test.tsx +++ b/frontend/__tests__/unit/components/Badges.test.tsx @@ -71,8 +71,6 @@ jest.mock('@heroui/tooltip', () => ({ describe('Badges', () => { const defaultProps = { name: 'Test Badge', - weight: 1, - description: 'Test description', cssClass: 'fa-medal', } @@ -116,7 +114,7 @@ describe('Badges', () => { expect(screen.getByTestId('icon-crown')).toBeInTheDocument() }) - it('renders correct icon for cssClass with multiple fa- prefixes', () => { + it('renders fallback medal icon for cssClass with multiple fa- prefixes', () => { render() // Should fall back to medal icon since 'fa-crown' is not a valid FontAwesome icon @@ -144,15 +142,6 @@ describe('Badges', () => { ) }) - it('shows fallback tooltip content for invalid icon', () => { - render() - - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'Test Badge (icon not found)' - ) - }) - it('renders fallback icon without tooltip when showTooltip is false', () => { render() @@ -182,7 +171,7 @@ describe('Badges', () => { expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Special Badge') }) - it('displays modified tooltip content for invalid icon', () => { + it('displays badge name for invalid icon with fallback message', () => { render() expect(screen.getByTestId('tooltip')).toHaveAttribute( @@ -224,7 +213,7 @@ describe('Badges', () => { expect(screen.getByTestId('icon-user-tie')).toBeInTheDocument() }) - it('handles cssClass with underscores', () => { + it('handles cssClass with underscores (falls back to medal)', () => { render() // The component should fall back to medal icon since 'user_tie' is not a valid FontAwesome icon @@ -233,7 +222,7 @@ describe('Badges', () => { }) describe('Styling and Classes', () => { - it('applies correct classes to valid icon', () => { + it('applies correct classes to icon', () => { render() const icon = screen.getByTestId('icon-medal') diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 2f1f5ac732..14fe848410 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -186,15 +186,15 @@ const UserDetailsPage: React.FC = () => { } const UserSummary = () => ( -
+
{user?.name -
+
@{user?.login} diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 58be15a1f7..e4fff067b9 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -18,8 +18,64 @@ type BadgeProps = { } const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { - const safeCssClass = String(cssClass ?? 'fa-medal').trim() - const tokens = safeCssClass.split(/\s+/) + if (cssClass == null) { + return ( +
+ {showTooltip ? ( + + + + ) : ( + + )} +
+ ) + } + if (typeof cssClass !== 'string') { + return ( +
+ {showTooltip ? ( + + + + ) : ( + + )} +
+ ) + } + + // Empty string should gracefully fall back to medal without error text + if (cssClass === '') { + return ( +
+ {showTooltip ? ( + + + + ) : ( + + )} +
+ ) + } + + const trimmed = String(cssClass).trim() + if (trimmed.length === 0) { + return ( +
+ {showTooltip ? ( + + + + ) : ( + + )} +
+ ) + } + + const tokens = trimmed.split(/\s+/) const styleToken = tokens.find((t) => /^fa-(solid|regular|brands)$/i.test(t)) const stylePrefix: IconLookup['prefix'] = styleToken?.toLowerCase() === 'fa-regular' @@ -27,13 +83,20 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { : styleToken?.toLowerCase() === 'fa-brands' ? 'fab' : 'fas' - const iconToken = - tokens - .slice() - .reverse() - .find((t) => /^fa-[a-z0-9-]+$/i.test(t) && !/^fa-(solid|regular|brands)$/i.test(t)) || - 'fa-medal' - const iconName = iconToken.replace(/^fa-/, '').replace(/_/g, '-') as IconName + + // Allow either a plain icon token (e.g., "crown") or an fa- prefixed token (e.g., "fa-crown") + const plainIconToken = tokens + .slice() + .reverse() + .find((t) => /^[a-z0-9-]+$/i.test(t)) + + const faIconToken = tokens + .slice() + .reverse() + .find((t) => /^fa-[a-z0-9-]+$/i.test(t) && !/^fa-(solid|regular|brands)$/i.test(t)) + + const chosenToken = plainIconToken || faIconToken || 'fa-medal' + const iconName = chosenToken.replace(/^fa-/, '') as IconName // Check if the icon exists in the FA library for the derived style const lookup: IconLookup = { prefix: stylePrefix, iconName } @@ -46,7 +109,6 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { } if (!iconFound) { - // Fallback to a default icon if the specified icon doesn't exist return (
{showTooltip ? ( From 1b445b071eb279566a0454889027798ee289ce16 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 19 Sep 2025 10:42:01 +0000 Subject: [PATCH 20/37] fix: correct badge relationship reference in UserNode and update tests - Fixed self.badges to self.user_badges in badges() method - Fixed self.badges to self.user_badges in badge_count() method - Updated all test mocks to use user_badges instead of badges - This resolves the incorrect relationship reference for badge filtering --- backend/apps/github/api/internal/nodes/user.py | 4 ++-- .../tests/apps/github/api/internal/nodes/user_test.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index 8b6309efbf..bf4dcda0af 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -32,7 +32,7 @@ class UserNode: def badges(self) -> list[BadgeNode]: """List badges assigned to the user sorted by weight and name.""" user_badges = ( - self.badges.filter(is_active=True) + self.user_badges.filter(is_active=True) .select_related("badge") .order_by("-badge__weight", "badge__name") ) @@ -41,7 +41,7 @@ def badges(self) -> list[BadgeNode]: @strawberry.field def badge_count(self) -> int: """Resolve badge count.""" - return self.badges.filter(is_active=True).values("badge_id").distinct().count() + return self.user_badges.filter(is_active=True).values("badge_id").distinct().count() @strawberry.field def created_at(self) -> float: 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 0c417ac39e..306eb1f93e 100644 --- a/backend/tests/apps/github/api/internal/nodes/user_test.py +++ b/backend/tests/apps/github/api/internal/nodes/user_test.py @@ -90,7 +90,7 @@ def test_badge_count_field(self): ( mock_badges_queryset.filter.return_value.values.return_value.distinct.return_value.count.return_value ) = 3 - mock_user.badges = mock_badges_queryset + mock_user.user_badges = mock_badges_queryset result = UserNode.badge_count(mock_user) assert result == 3 @@ -105,7 +105,7 @@ def test_badges_field_empty(self): ( mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value ) = [] - mock_user.badges = mock_badges_queryset + mock_user.user_badges = mock_badges_queryset result = UserNode.badges(mock_user) assert result == [] @@ -121,7 +121,7 @@ def test_badges_field_single_badge(self): ( mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value ) = [mock_user_badge] - mock_user.badges = mock_badges_queryset + mock_user.user_badges = mock_badges_queryset result = UserNode.badges(mock_user) assert result == [mock_badge] @@ -170,7 +170,7 @@ def test_badges_field_sorted_by_weight_and_name(self): mock_user_badge_low, # weight 10 ] mock_user = Mock() - mock_user.badges = mock_badges_queryset + mock_user.user_badges = mock_badges_queryset result = UserNode.badges(mock_user) @@ -197,7 +197,7 @@ def test_badges_field_filters_inactive_badges(self): ( mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value ) = [] - mock_user.badges = mock_badges_queryset + mock_user.user_badges = mock_badges_queryset UserNode.badges(mock_user) From 263d53f88b0c3d6ae9dd053fb67e69bbcba29bc2 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE Date: Fri, 19 Sep 2025 16:35:59 +0530 Subject: [PATCH 21/37] refactor: streamline badge display in UserDetailsPage component --- frontend/src/app/members/[memberKey]/page.tsx | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 14fe848410..5e333bab31 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -195,26 +195,27 @@ const UserDetailsPage: React.FC = () => { alt={user?.name || user?.login || 'User Avatar'} />
- - @{user?.login} - +
+ + @{user?.login} + + {user?.badges && user.badges.length > 0 && ( +
+ {user.badges + .slice() + .sort((a, b) => b.weight - a.weight) + .map((badge) => ( + + ))} +
+ )} +

{formattedBio}

- {user?.badges && user.badges.length > 0 && ( -
- Badges: - {user.badges - .slice() - .sort((a, b) => b.weight - a.weight) - .map((badge) => ( - - ))} -
- )} {!isPrivateContributor && (
From f429bbf737b4e4d8b39702ca0a01e4407c04ac9b Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE Date: Fri, 19 Sep 2025 17:15:39 +0530 Subject: [PATCH 22/37] fix: update useParams to use memberKey in UserDetailsPage and adjust test mocks --- .../__tests__/unit/pages/UserDetails.test.tsx | 210 +++++++++--------- frontend/src/app/members/[memberKey]/page.tsx | 4 +- 2 files changed, 107 insertions(+), 107 deletions(-) diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 16c7b7e626..cfbb124446 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -66,13 +66,13 @@ 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 jest.mock('utils/helpers/githubHeatmap', () => ({ fetchHeatmapData: jest.fn(), - drawContributions: jest.fn(() => {}), + drawContributions: jest.fn(() => { }), })) jest.mock('@heroui/toast', () => ({ @@ -81,15 +81,15 @@ jest.mock('@heroui/toast', () => ({ describe('UserDetailsPage', () => { beforeEach(() => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, }) - ;(fetchHeatmapData as jest.Mock).mockResolvedValue({ - contributions: { years: [{ year: '2023' }] }, - }) - ;(drawContributions as jest.Mock).mockImplementation(() => {}) + ; (fetchHeatmapData as jest.Mock).mockResolvedValue({ + contributions: { years: [{ year: '2023' }] }, + }) + ; (drawContributions as jest.Mock).mockImplementation(() => { }) }) afterEach(() => { @@ -97,7 +97,7 @@ describe('UserDetailsPage', () => { }) test('renders loading state', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: null, error: null, }) @@ -110,7 +110,7 @@ describe('UserDetailsPage', () => { }) test('renders user details', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, recentIssues: {} }, @@ -136,7 +136,7 @@ describe('UserDetailsPage', () => { }) test('renders recent issues correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -157,7 +157,7 @@ describe('UserDetailsPage', () => { }) test('renders recent pull requests correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -175,7 +175,7 @@ describe('UserDetailsPage', () => { }) test('renders recent releases correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -193,7 +193,7 @@ describe('UserDetailsPage', () => { }) test('renders recent milestones correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -215,7 +215,7 @@ describe('UserDetailsPage', () => { }) test('renders repositories section correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -239,7 +239,7 @@ describe('UserDetailsPage', () => { }) test('renders statistics section correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -266,14 +266,14 @@ describe('UserDetailsPage', () => { }) test('renders contribution heatmap correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, }) - ;(fetchHeatmapData as jest.Mock).mockResolvedValue({ - years: [{ year: '2023' }], // Provide years data to satisfy condition in component - }) + ; (fetchHeatmapData as jest.Mock).mockResolvedValue({ + years: [{ year: '2023' }], // Provide years data to satisfy condition in component + }) render() @@ -289,11 +289,11 @@ describe('UserDetailsPage', () => { }) test('handles contribution heatmap loading error correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) - ;(fetchHeatmapData as jest.Mock).mockResolvedValue(null) + ; (fetchHeatmapData as jest.Mock).mockResolvedValue(null) render() @@ -304,7 +304,7 @@ describe('UserDetailsPage', () => { }) test('renders user summary section correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -327,7 +327,7 @@ describe('UserDetailsPage', () => { }) test('displays contact information elements', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -347,7 +347,7 @@ describe('UserDetailsPage', () => { }) test('renders error message when GraphQL request fails', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: null, error: mockError, }) @@ -373,11 +373,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, bio: '' }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noBioData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noBioData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -391,11 +391,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, bio: 'Test @User1 and @User2!' }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: multiMentionData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: multiMentionData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -410,11 +410,11 @@ describe('UserDetailsPage', () => { const noIssuesData = { user: { ...mockUserDetailsData.user, recentIssues: {} }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noIssuesData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noIssuesData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -428,11 +428,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentPullRequests: [], } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noPullsData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noPullsData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -446,11 +446,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentReleases: [], } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noReleasesData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noReleasesData, + loading: false, + error: null, + }) render() await waitFor(() => { expect(screen.getByText('Recent Releases')).toBeInTheDocument() @@ -463,11 +463,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentMilestones: [], } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noMilestonesData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noMilestonesData, + loading: false, + error: null, + }) render() await waitFor(() => { expect(screen.getByText('Recent Milestones')).toBeInTheDocument() @@ -486,11 +486,11 @@ describe('UserDetailsPage', () => { publicRepositoriesCount: 0, }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: zeroStatsData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: zeroStatsData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -511,11 +511,11 @@ describe('UserDetailsPage', () => { location: '', }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: minimalData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: minimalData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -526,7 +526,7 @@ describe('UserDetailsPage', () => { }) }) test('does not render sponsor block', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -538,7 +538,7 @@ describe('UserDetailsPage', () => { describe('Badge Display Tests', () => { test('renders badges section when user has badges', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -546,14 +546,13 @@ describe('UserDetailsPage', () => { render() await waitFor(() => { - expect(screen.getByText('Badges:')).toBeInTheDocument() expect(screen.getByTestId('badge-contributor')).toBeInTheDocument() expect(screen.getByTestId('badge-security-expert')).toBeInTheDocument() }) }) test('renders badges with correct props', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -581,15 +580,15 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithoutBadges, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) render() await waitFor(() => { - expect(screen.queryByText('Badges:')).not.toBeInTheDocument() + expect(screen.queryByTestId(/^badge-/)).not.toBeInTheDocument() }) }) @@ -603,15 +602,15 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithoutBadges, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) render() await waitFor(() => { - expect(screen.queryByText('Badges:')).not.toBeInTheDocument() + expect(screen.queryByTestId(/^badge-/)).not.toBeInTheDocument() }) }) @@ -632,11 +631,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithIncompleteBadges, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithIncompleteBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -662,11 +661,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithEmptyCssClass, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithEmptyCssClass, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -676,7 +675,7 @@ describe('UserDetailsPage', () => { }) test('renders multiple badges in correct order', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -684,12 +683,13 @@ describe('UserDetailsPage', () => { render() await waitFor(() => { - const badgesContainer = screen.getByText('Badges:').parentElement - const badges = badgesContainer?.querySelectorAll('[data-testid^="badge-"]') + const badges = screen.getAllByTestId(/^badge-/) expect(badges).toHaveLength(2) - expect(badges?.[0]).toHaveAttribute('data-testid', 'badge-security-expert') - expect(badges?.[1]).toHaveAttribute('data-testid', 'badge-contributor') + // Security Expert has weight 2, should come first + expect(badges[0]).toHaveAttribute('data-testid', 'badge-security-expert') + // Contributor has weight 1, should come second + expect(badges[1]).toHaveAttribute('data-testid', 'badge-contributor') }) }) @@ -710,11 +710,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithSpecialBadges, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithSpecialBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -739,11 +739,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithLongNameBadge, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithLongNameBadge, + loading: false, + error: null, + }) render() await waitFor(() => { diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 5e333bab31..bf773baddb 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -195,7 +195,7 @@ const UserDetailsPage: React.FC = () => { alt={user?.name || user?.login || 'User Avatar'} />
-
+
@{user?.login} @@ -215,7 +215,7 @@ const UserDetailsPage: React.FC = () => {
)}
-

{formattedBio}

+

{formattedBio}

{!isPrivateContributor && (
From 5c698637e6bb1018f011d1fda31e82312ed24281 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE Date: Fri, 19 Sep 2025 17:20:47 +0530 Subject: [PATCH 23/37] fix: adjust class names for better layout consistency in UserDetailsPage --- .../__tests__/unit/pages/UserDetails.test.tsx | 194 +++++++++--------- frontend/src/app/members/[memberKey]/page.tsx | 4 +- 2 files changed, 99 insertions(+), 99 deletions(-) diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index cfbb124446..32b4a7189a 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -72,7 +72,7 @@ jest.mock('next/navigation', () => ({ // Mock GitHub heatmap utilities jest.mock('utils/helpers/githubHeatmap', () => ({ fetchHeatmapData: jest.fn(), - drawContributions: jest.fn(() => { }), + drawContributions: jest.fn(() => {}), })) jest.mock('@heroui/toast', () => ({ @@ -81,15 +81,15 @@ jest.mock('@heroui/toast', () => ({ describe('UserDetailsPage', () => { beforeEach(() => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, }) - ; (fetchHeatmapData as jest.Mock).mockResolvedValue({ - contributions: { years: [{ year: '2023' }] }, - }) - ; (drawContributions as jest.Mock).mockImplementation(() => { }) + ;(fetchHeatmapData as jest.Mock).mockResolvedValue({ + contributions: { years: [{ year: '2023' }] }, + }) + ;(drawContributions as jest.Mock).mockImplementation(() => {}) }) afterEach(() => { @@ -97,7 +97,7 @@ describe('UserDetailsPage', () => { }) test('renders loading state', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: null, error: null, }) @@ -110,7 +110,7 @@ describe('UserDetailsPage', () => { }) test('renders user details', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, recentIssues: {} }, @@ -136,7 +136,7 @@ describe('UserDetailsPage', () => { }) test('renders recent issues correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -157,7 +157,7 @@ describe('UserDetailsPage', () => { }) test('renders recent pull requests correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -175,7 +175,7 @@ describe('UserDetailsPage', () => { }) test('renders recent releases correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -193,7 +193,7 @@ describe('UserDetailsPage', () => { }) test('renders recent milestones correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -215,7 +215,7 @@ describe('UserDetailsPage', () => { }) test('renders repositories section correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -239,7 +239,7 @@ describe('UserDetailsPage', () => { }) test('renders statistics section correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -266,14 +266,14 @@ describe('UserDetailsPage', () => { }) test('renders contribution heatmap correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, }) - ; (fetchHeatmapData as jest.Mock).mockResolvedValue({ - years: [{ year: '2023' }], // Provide years data to satisfy condition in component - }) + ;(fetchHeatmapData as jest.Mock).mockResolvedValue({ + years: [{ year: '2023' }], // Provide years data to satisfy condition in component + }) render() @@ -289,11 +289,11 @@ describe('UserDetailsPage', () => { }) test('handles contribution heatmap loading error correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) - ; (fetchHeatmapData as jest.Mock).mockResolvedValue(null) + ;(fetchHeatmapData as jest.Mock).mockResolvedValue(null) render() @@ -304,7 +304,7 @@ describe('UserDetailsPage', () => { }) test('renders user summary section correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -327,7 +327,7 @@ describe('UserDetailsPage', () => { }) test('displays contact information elements', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -347,7 +347,7 @@ describe('UserDetailsPage', () => { }) test('renders error message when GraphQL request fails', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: null, error: mockError, }) @@ -373,11 +373,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, bio: '' }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noBioData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noBioData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -391,11 +391,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, bio: 'Test @User1 and @User2!' }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: multiMentionData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: multiMentionData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -410,11 +410,11 @@ describe('UserDetailsPage', () => { const noIssuesData = { user: { ...mockUserDetailsData.user, recentIssues: {} }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noIssuesData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noIssuesData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -428,11 +428,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentPullRequests: [], } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noPullsData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noPullsData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -446,11 +446,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentReleases: [], } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noReleasesData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noReleasesData, + loading: false, + error: null, + }) render() await waitFor(() => { expect(screen.getByText('Recent Releases')).toBeInTheDocument() @@ -463,11 +463,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentMilestones: [], } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noMilestonesData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noMilestonesData, + loading: false, + error: null, + }) render() await waitFor(() => { expect(screen.getByText('Recent Milestones')).toBeInTheDocument() @@ -486,11 +486,11 @@ describe('UserDetailsPage', () => { publicRepositoriesCount: 0, }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: zeroStatsData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: zeroStatsData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -511,11 +511,11 @@ describe('UserDetailsPage', () => { location: '', }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: minimalData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: minimalData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -526,7 +526,7 @@ describe('UserDetailsPage', () => { }) }) test('does not render sponsor block', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -538,7 +538,7 @@ describe('UserDetailsPage', () => { describe('Badge Display Tests', () => { test('renders badges section when user has badges', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -552,7 +552,7 @@ describe('UserDetailsPage', () => { }) test('renders badges with correct props', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -580,11 +580,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithoutBadges, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -602,11 +602,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithoutBadges, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -631,11 +631,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithIncompleteBadges, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithIncompleteBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -661,11 +661,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithEmptyCssClass, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithEmptyCssClass, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -675,7 +675,7 @@ describe('UserDetailsPage', () => { }) test('renders multiple badges in correct order', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -710,11 +710,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithSpecialBadges, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithSpecialBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -739,11 +739,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithLongNameBadge, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithLongNameBadge, + loading: false, + error: null, + }) render() await waitFor(() => { diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index bf773baddb..218bf2f437 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -195,7 +195,7 @@ const UserDetailsPage: React.FC = () => { alt={user?.name || user?.login || 'User Avatar'} />
-
+
@{user?.login} @@ -215,7 +215,7 @@ const UserDetailsPage: React.FC = () => {
)}
-

{formattedBio}

+

{formattedBio}

{!isPrivateContributor && (
From f9e044d0888b7b62addd0fefd001a2c9d45beff9 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 21 Sep 2025 10:45:35 +0000 Subject: [PATCH 24/37] Revert "fix: adjust class names for better layout consistency in UserDetailsPage" This reverts commit 5c698637e6bb1018f011d1fda31e82312ed24281. --- .../__tests__/unit/pages/UserDetails.test.tsx | 194 +++++++++--------- frontend/src/app/members/[memberKey]/page.tsx | 4 +- 2 files changed, 99 insertions(+), 99 deletions(-) diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 32b4a7189a..cfbb124446 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -72,7 +72,7 @@ jest.mock('next/navigation', () => ({ // Mock GitHub heatmap utilities jest.mock('utils/helpers/githubHeatmap', () => ({ fetchHeatmapData: jest.fn(), - drawContributions: jest.fn(() => {}), + drawContributions: jest.fn(() => { }), })) jest.mock('@heroui/toast', () => ({ @@ -81,15 +81,15 @@ jest.mock('@heroui/toast', () => ({ describe('UserDetailsPage', () => { beforeEach(() => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, }) - ;(fetchHeatmapData as jest.Mock).mockResolvedValue({ - contributions: { years: [{ year: '2023' }] }, - }) - ;(drawContributions as jest.Mock).mockImplementation(() => {}) + ; (fetchHeatmapData as jest.Mock).mockResolvedValue({ + contributions: { years: [{ year: '2023' }] }, + }) + ; (drawContributions as jest.Mock).mockImplementation(() => { }) }) afterEach(() => { @@ -97,7 +97,7 @@ describe('UserDetailsPage', () => { }) test('renders loading state', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: null, error: null, }) @@ -110,7 +110,7 @@ describe('UserDetailsPage', () => { }) test('renders user details', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, recentIssues: {} }, @@ -136,7 +136,7 @@ describe('UserDetailsPage', () => { }) test('renders recent issues correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -157,7 +157,7 @@ describe('UserDetailsPage', () => { }) test('renders recent pull requests correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -175,7 +175,7 @@ describe('UserDetailsPage', () => { }) test('renders recent releases correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -193,7 +193,7 @@ describe('UserDetailsPage', () => { }) test('renders recent milestones correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -215,7 +215,7 @@ describe('UserDetailsPage', () => { }) test('renders repositories section correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -239,7 +239,7 @@ describe('UserDetailsPage', () => { }) test('renders statistics section correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -266,14 +266,14 @@ describe('UserDetailsPage', () => { }) test('renders contribution heatmap correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, }) - ;(fetchHeatmapData as jest.Mock).mockResolvedValue({ - years: [{ year: '2023' }], // Provide years data to satisfy condition in component - }) + ; (fetchHeatmapData as jest.Mock).mockResolvedValue({ + years: [{ year: '2023' }], // Provide years data to satisfy condition in component + }) render() @@ -289,11 +289,11 @@ describe('UserDetailsPage', () => { }) test('handles contribution heatmap loading error correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) - ;(fetchHeatmapData as jest.Mock).mockResolvedValue(null) + ; (fetchHeatmapData as jest.Mock).mockResolvedValue(null) render() @@ -304,7 +304,7 @@ describe('UserDetailsPage', () => { }) test('renders user summary section correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -327,7 +327,7 @@ describe('UserDetailsPage', () => { }) test('displays contact information elements', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -347,7 +347,7 @@ describe('UserDetailsPage', () => { }) test('renders error message when GraphQL request fails', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: null, error: mockError, }) @@ -373,11 +373,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, bio: '' }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noBioData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noBioData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -391,11 +391,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, bio: 'Test @User1 and @User2!' }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: multiMentionData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: multiMentionData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -410,11 +410,11 @@ describe('UserDetailsPage', () => { const noIssuesData = { user: { ...mockUserDetailsData.user, recentIssues: {} }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noIssuesData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noIssuesData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -428,11 +428,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentPullRequests: [], } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noPullsData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noPullsData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -446,11 +446,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentReleases: [], } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noReleasesData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noReleasesData, + loading: false, + error: null, + }) render() await waitFor(() => { expect(screen.getByText('Recent Releases')).toBeInTheDocument() @@ -463,11 +463,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentMilestones: [], } - ;(useQuery as jest.Mock).mockReturnValue({ - data: noMilestonesData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: noMilestonesData, + loading: false, + error: null, + }) render() await waitFor(() => { expect(screen.getByText('Recent Milestones')).toBeInTheDocument() @@ -486,11 +486,11 @@ describe('UserDetailsPage', () => { publicRepositoriesCount: 0, }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: zeroStatsData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: zeroStatsData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -511,11 +511,11 @@ describe('UserDetailsPage', () => { location: '', }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: minimalData, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: minimalData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -526,7 +526,7 @@ describe('UserDetailsPage', () => { }) }) test('does not render sponsor block', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -538,7 +538,7 @@ describe('UserDetailsPage', () => { describe('Badge Display Tests', () => { test('renders badges section when user has badges', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -552,7 +552,7 @@ describe('UserDetailsPage', () => { }) test('renders badges with correct props', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -580,11 +580,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithoutBadges, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -602,11 +602,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithoutBadges, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -631,11 +631,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithIncompleteBadges, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithIncompleteBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -661,11 +661,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithEmptyCssClass, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithEmptyCssClass, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -675,7 +675,7 @@ describe('UserDetailsPage', () => { }) test('renders multiple badges in correct order', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -710,11 +710,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithSpecialBadges, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithSpecialBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -739,11 +739,11 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ - data: dataWithLongNameBadge, - loading: false, - error: null, - }) + ; (useQuery as jest.Mock).mockReturnValue({ + data: dataWithLongNameBadge, + loading: false, + error: null, + }) render() await waitFor(() => { diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 218bf2f437..bf773baddb 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -195,7 +195,7 @@ const UserDetailsPage: React.FC = () => { alt={user?.name || user?.login || 'User Avatar'} />
-
+
@{user?.login} @@ -215,7 +215,7 @@ const UserDetailsPage: React.FC = () => {
)}
-

{formattedBio}

+

{formattedBio}

{!isPrivateContributor && (
From f42907165715179ca7fe565f5eec6c1b6b164a15 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE Date: Mon, 22 Sep 2025 23:55:49 +0530 Subject: [PATCH 25/37] Refactor UserNode badge logic and improve Badge component rendering - Updated UserNode to remove unnecessary filters for badge retrieval and count. - Modified BadgeNode to inherit from Node and adjusted fields accordingly. - Enhanced unit tests for UserNode to reflect changes in badge logic. - Simplified Badges component by removing FontAwesome dependencies and using a wrapper for icon rendering. - Improved UserDetailsPage to utilize badgeCount directly from user data. - Adjusted UsersPage to reference badgeCount instead of calculating length. - Updated GraphQL types to ensure BadgeNode extends Node for consistency. --- .../apps/github/api/internal/nodes/user.py | 8 +- backend/apps/nest/api/internal/nodes/badge.py | 9 +- .../github/api/internal/nodes/user_test.py | 49 +-- .../__tests__/unit/components/Badges.test.tsx | 334 ++---------------- .../__tests__/unit/pages/UserDetails.test.tsx | 210 +++++------ frontend/src/app/members/[memberKey]/page.tsx | 15 +- frontend/src/app/members/page.tsx | 2 +- frontend/src/components/Badges.tsx | 128 +------ frontend/src/types/__generated__/graphql.ts | 2 +- 9 files changed, 184 insertions(+), 573 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index bf4dcda0af..228929d616 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -31,17 +31,15 @@ class UserNode: @strawberry.field def badges(self) -> list[BadgeNode]: """List badges assigned to the user sorted by weight and name.""" - user_badges = ( - self.user_badges.filter(is_active=True) - .select_related("badge") - .order_by("-badge__weight", "badge__name") + user_badges = self.user_badges.select_related("badge").order_by( + "-badge__weight", "badge__name" ) return [user_badge.badge for user_badge in user_badges] @strawberry.field def badge_count(self) -> int: """Resolve badge count.""" - return self.user_badges.filter(is_active=True).values("badge_id").distinct().count() + return self.user_badges.values("badge_id").distinct().count() @strawberry.field def created_at(self) -> float: diff --git a/backend/apps/nest/api/internal/nodes/badge.py b/backend/apps/nest/api/internal/nodes/badge.py index 0c6fdcbd47..da4506e49c 100644 --- a/backend/apps/nest/api/internal/nodes/badge.py +++ b/backend/apps/nest/api/internal/nodes/badge.py @@ -1,19 +1,20 @@ """GraphQL node for Badge model.""" +import strawberry import strawberry_django -from apps.nest.models import Badge +from apps.nest.models.badge import Badge @strawberry_django.type( Badge, fields=[ + "css_class", + "description", "id", "name", - "description", "weight", - "css_class", ], ) -class BadgeNode: +class BadgeNode(strawberry.relay.Node): """GraphQL node for Badge model.""" 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 306eb1f93e..72d77a7d73 100644 --- a/backend/tests/apps/github/api/internal/nodes/user_test.py +++ b/backend/tests/apps/github/api/internal/nodes/user_test.py @@ -87,28 +87,28 @@ def test_badge_count_field(self): """Test badge_count field resolution.""" mock_user = Mock() mock_badges_queryset = Mock() - ( - mock_badges_queryset.filter.return_value.values.return_value.distinct.return_value.count.return_value - ) = 3 + mock_badges_queryset.values.return_value.distinct.return_value.count.return_value = 3 mock_user.user_badges = mock_badges_queryset 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.values.assert_called_once_with("badge_id") - mock_badges_queryset.filter.return_value.values.return_value.distinct.assert_called_once() + mock_badges_queryset.values.assert_called_once_with("badge_id") + mock_badges_queryset.values.return_value.distinct.assert_called_once() + mock_badges_queryset.values.return_value.distinct.return_value.count.assert_called_once() def test_badges_field_empty(self): """Test badges field resolution with no badges.""" mock_user = Mock() mock_badges_queryset = Mock() - ( - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value - ) = [] + mock_badges_queryset.select_related.return_value.order_by.return_value = [] mock_user.user_badges = mock_badges_queryset result = UserNode.badges(mock_user) assert result == [] + mock_badges_queryset.select_related.assert_called_once_with("badge") + mock_badges_queryset.select_related.return_value.order_by.assert_called_once_with( + "-badge__weight", "badge__name" + ) def test_badges_field_single_badge(self): """Test badges field resolution with single badge.""" @@ -118,13 +118,15 @@ def test_badges_field_single_badge(self): mock_user_badge.badge = mock_badge mock_badges_queryset = Mock() - ( - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value - ) = [mock_user_badge] + mock_badges_queryset.select_related.return_value.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.select_related.assert_called_once_with("badge") + mock_badges_queryset.select_related.return_value.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.""" @@ -161,9 +163,7 @@ def test_badges_field_sorted_by_weight_and_name(self): # Set up the mock queryset to return badges in the expected sorted order # (highest weight first, then by name for same weight) mock_badges_queryset = Mock() - ( - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value - ) = [ + mock_badges_queryset.select_related.return_value.order_by.return_value = [ mock_user_badge_high, # weight 100 mock_user_badge_medium_a, # weight 50, name "Medium Weight A" mock_user_badge_medium_b, # weight 50, name "Medium Weight B" @@ -184,22 +184,7 @@ def test_badges_field_sorted_by_weight_and_name(self): assert result == expected_badges # Verify the queryset was called with correct ordering - mock_badges_queryset.filter.assert_called_once_with(is_active=True) - mock_badges_queryset.filter.return_value.select_related.assert_called_once_with("badge") - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.assert_called_once_with( + mock_badges_queryset.select_related.assert_called_once_with("badge") + mock_badges_queryset.select_related.return_value.order_by.assert_called_once_with( "-badge__weight", "badge__name" ) - - def test_badges_field_filters_inactive_badges(self): - """Test badges field only returns active badges.""" - mock_user = Mock() - mock_badges_queryset = Mock() - ( - mock_badges_queryset.filter.return_value.select_related.return_value.order_by.return_value - ) = [] - mock_user.user_badges = mock_badges_queryset - - UserNode.badges(mock_user) - - # Verify that only active badges are filtered - mock_badges_queryset.filter.assert_called_once_with(is_active=True) diff --git a/frontend/__tests__/unit/components/Badges.test.tsx b/frontend/__tests__/unit/components/Badges.test.tsx index ae9973a2c2..f05b0e1d37 100644 --- a/frontend/__tests__/unit/components/Badges.test.tsx +++ b/frontend/__tests__/unit/components/Badges.test.tsx @@ -1,330 +1,72 @@ -import { render, screen, cleanup } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import React from 'react' import Badges from 'components/Badges' -// Mock FontAwesome components -jest.mock('@fortawesome/react-fontawesome', () => ({ - FontAwesomeIcon: ({ - icon, - className, - ...props - }: { - icon: string[] | { iconName: string } - className?: string - [key: string]: unknown - }) => { - const iconName = Array.isArray(icon) ? icon[1] : icon.iconName - return - }, -})) - -// Mock FontAwesome library +// Mock FontAwesome jest.mock('@fortawesome/fontawesome-svg-core', () => ({ - library: { add: jest.fn() }, - // Simulate FA lookup; throw if not found findIconDefinition: jest.fn(({ iconName }: { iconName: string }) => { - const valid = new Set([ - 'medal', - 'shield-alt', - 'code', - 'user-graduate', - 'crown', - 'star', - 'trophy', - 'user-tie', - ]) - if (valid.has(iconName)) return { iconName } - throw new Error('not found') + // Return truthy value for valid icons, null for invalid + return iconName === 'medal' ? { iconName } : null }), })) -// Mock FontAwesome icons -jest.mock('@fortawesome/free-solid-svg-icons', () => ({ - fas: { - medal: { iconName: 'medal' }, - 'shield-alt': { iconName: 'shield-alt' }, - code: { iconName: 'code' }, - 'user-graduate': { iconName: 'user-graduate' }, - crown: { iconName: 'crown' }, - star: { iconName: 'star' }, - trophy: { iconName: 'trophy' }, - 'user-tie': { iconName: 'user-tie' }, - }, -})) +jest.mock('wrappers/FontAwesomeIconWrapper', () => { + return function MockFontAwesomeIconWrapper({ icon }: { icon: string }) { + const iconName = icon.split(' ').pop()?.replace('fa-', '') || icon.replace('fa-', '') + return + } +}) jest.mock('@heroui/tooltip', () => ({ Tooltip: ({ children, content, - ...props + isDisabled, }: { children: React.ReactNode content: string - [key: string]: unknown - }) => ( -
- {children} -
- ), + isDisabled?: boolean + }) => { + if (isDisabled) { + return <>{children} + } + return ( +
+ {children} +
+ ) + }, })) -describe('Badges', () => { +describe('Badges Component', () => { const defaultProps = { name: 'Test Badge', - cssClass: 'fa-medal', + cssClass: 'fa-solid fa-medal', } - afterEach(() => { - cleanup() - }) - - describe('Essential Rendering Tests', () => { - it('renders successfully with valid icon', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') - }) + it('renders valid icon with tooltip', () => { + render() - it('renders without tooltip when showTooltip is false', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument() - }) - - it('renders with tooltip by default when showTooltip is not specified', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toBeInTheDocument() - }) + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') }) - describe('Icon Handling', () => { - it('renders correct icon for valid cssClass', () => { - render() - - expect(screen.getByTestId('icon-crown')).toBeInTheDocument() - }) + it('renders fallback fa-medal for invalid cssClass', () => { + render() - it('renders correct icon for cssClass without fa- prefix', () => { - render() - - expect(screen.getByTestId('icon-crown')).toBeInTheDocument() - }) - - it('renders fallback medal icon for cssClass with multiple fa- prefixes', () => { - render() - - // Should fall back to medal icon since 'fa-crown' is not a valid FontAwesome icon - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - }) - - it('handles different valid icons', () => { - const icons = ['fa-medal', 'fa-crown', 'fa-star', 'fa-trophy'] - icons.forEach((icon) => { - const { unmount } = render() - expect(screen.getByTestId(`icon-${icon.replace('fa-', '')}`)).toBeInTheDocument() - unmount() - }) - }) + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() }) - describe('Fallback Behavior', () => { - it('renders fallback medal icon for invalid cssClass', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'Test Badge (icon not found)' - ) - }) - - it('renders fallback icon without tooltip when showTooltip is false', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument() - }) - - it('handles empty cssClass gracefully', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') - }) + it('renders fa-question fallback for unrecognized icon', () => { + render() - it('handles undefined cssClass gracefully', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') - }) + expect(screen.getByTestId('icon-question')).toBeInTheDocument() }) - describe('Tooltip Content', () => { - it('displays badge name in tooltip for valid icon', () => { - render() - - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Special Badge') - }) - - it('displays badge name for invalid icon with fallback message', () => { - render() - - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'Invalid Badge (icon not found)' - ) - }) - - it('handles special characters in badge name', () => { - render() - - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Badge & More!') - }) - - it('handles long badge names', () => { - const longName = 'Very Long Badge Name That Exceeds Normal Length' - render() - - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', longName) - }) - }) - - describe('CSS Class Processing', () => { - it('correctly processes cssClass with fa- prefix', () => { - render() - - expect(screen.getByTestId('icon-crown')).toBeInTheDocument() - }) - - it('correctly processes cssClass without fa- prefix', () => { - render() - - expect(screen.getByTestId('icon-crown')).toBeInTheDocument() - }) - - it('handles cssClass with multiple dashes', () => { - render() - - expect(screen.getByTestId('icon-user-tie')).toBeInTheDocument() - }) - - it('handles cssClass with underscores (falls back to medal)', () => { - render() - - // The component should fall back to medal icon since 'user_tie' is not a valid FontAwesome icon - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - }) - }) - - describe('Styling and Classes', () => { - it('applies correct classes to icon', () => { - render() - - const icon = screen.getByTestId('icon-medal') - expect(icon).toHaveClass('h-4', 'w-4') - }) - - it('applies correct classes to fallback icon', () => { - render() - - const icon = screen.getByTestId('icon-medal') - expect(icon).toHaveClass('h-4', 'w-4', 'text-gray-400') - }) - - it('wraps icon in inline-flex container', () => { - render() - - const container = screen.getByTestId('icon-medal').parentElement?.parentElement - expect(container).toHaveClass('inline-flex', 'items-center') - }) - }) - - describe('Edge Cases and Error Handling', () => { - it('handles null cssClass', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Test Badge') - }) - - it('handles numeric cssClass', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'Test Badge (icon not found)' - ) - }) - - it('handles boolean cssClass', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'Test Badge (icon not found)' - ) - }) - - it('handles whitespace-only cssClass', () => { - render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'Test Badge (icon not found)' - ) - }) - }) - - describe('Accessibility', () => { - it('provides tooltip content for screen readers', () => { - render() - - expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Accessible Badge') - }) - - it('maintains tooltip accessibility for fallback icons', () => { - render() - - expect(screen.getByTestId('tooltip')).toHaveAttribute( - 'data-content', - 'Fallback Badge (icon not found)' - ) - }) - }) - - describe('Performance and Optimization', () => { - it('renders efficiently with multiple badges', () => { - const badges = [ - { ...defaultProps, name: 'Badge 1', cssClass: 'fa-medal' }, - { ...defaultProps, name: 'Badge 2', cssClass: 'fa-crown' }, - { ...defaultProps, name: 'Badge 3', cssClass: 'fa-star' }, - ] - - badges.forEach((badge, _index) => { - const { unmount } = render() - expect(screen.getByTestId(`icon-${badge.cssClass.replace('fa-', '')}`)).toBeInTheDocument() - unmount() - }) - }) - - it('handles rapid re-renders with different props', () => { - const { rerender } = render() - - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - - rerender() - expect(screen.getByTestId('icon-crown')).toBeInTheDocument() + it('hides tooltip when showTooltip is false', () => { + render() - rerender() - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() - }) + expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument() }) }) diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index cfbb124446..7e3b6b204f 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -72,7 +72,7 @@ jest.mock('next/navigation', () => ({ // Mock GitHub heatmap utilities jest.mock('utils/helpers/githubHeatmap', () => ({ fetchHeatmapData: jest.fn(), - drawContributions: jest.fn(() => { }), + drawContributions: jest.fn(() => {}), })) jest.mock('@heroui/toast', () => ({ @@ -81,15 +81,15 @@ jest.mock('@heroui/toast', () => ({ describe('UserDetailsPage', () => { beforeEach(() => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, }) - ; (fetchHeatmapData as jest.Mock).mockResolvedValue({ - contributions: { years: [{ year: '2023' }] }, - }) - ; (drawContributions as jest.Mock).mockImplementation(() => { }) + ;(fetchHeatmapData as jest.Mock).mockResolvedValue({ + contributions: { years: [{ year: '2023' }] }, + }) + ;(drawContributions as jest.Mock).mockImplementation(() => {}) }) afterEach(() => { @@ -97,7 +97,7 @@ describe('UserDetailsPage', () => { }) test('renders loading state', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: null, error: null, }) @@ -110,7 +110,7 @@ describe('UserDetailsPage', () => { }) test('renders user details', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, recentIssues: {} }, @@ -136,7 +136,7 @@ describe('UserDetailsPage', () => { }) test('renders recent issues correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -157,7 +157,7 @@ describe('UserDetailsPage', () => { }) test('renders recent pull requests correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -175,7 +175,7 @@ describe('UserDetailsPage', () => { }) test('renders recent releases correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -193,7 +193,7 @@ describe('UserDetailsPage', () => { }) test('renders recent milestones correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -215,7 +215,7 @@ describe('UserDetailsPage', () => { }) test('renders repositories section correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -239,7 +239,7 @@ describe('UserDetailsPage', () => { }) test('renders statistics section correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, @@ -266,14 +266,14 @@ describe('UserDetailsPage', () => { }) test('renders contribution heatmap correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, loading: false, }) - ; (fetchHeatmapData as jest.Mock).mockResolvedValue({ - years: [{ year: '2023' }], // Provide years data to satisfy condition in component - }) + ;(fetchHeatmapData as jest.Mock).mockResolvedValue({ + years: [{ year: '2023' }], // Provide years data to satisfy condition in component + }) render() @@ -289,11 +289,11 @@ describe('UserDetailsPage', () => { }) test('handles contribution heatmap loading error correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) - ; (fetchHeatmapData as jest.Mock).mockResolvedValue(null) + ;(fetchHeatmapData as jest.Mock).mockResolvedValue(null) render() @@ -304,7 +304,7 @@ describe('UserDetailsPage', () => { }) test('renders user summary section correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -327,7 +327,7 @@ describe('UserDetailsPage', () => { }) test('displays contact information elements', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -347,7 +347,7 @@ describe('UserDetailsPage', () => { }) test('renders error message when GraphQL request fails', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: null, error: mockError, }) @@ -373,11 +373,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, bio: '' }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noBioData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noBioData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -391,11 +391,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, user: { ...mockUserDetailsData.user, bio: 'Test @User1 and @User2!' }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: multiMentionData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: multiMentionData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -410,11 +410,11 @@ describe('UserDetailsPage', () => { const noIssuesData = { user: { ...mockUserDetailsData.user, recentIssues: {} }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noIssuesData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noIssuesData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -428,11 +428,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentPullRequests: [], } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noPullsData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noPullsData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -446,11 +446,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentReleases: [], } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noReleasesData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noReleasesData, + loading: false, + error: null, + }) render() await waitFor(() => { expect(screen.getByText('Recent Releases')).toBeInTheDocument() @@ -463,11 +463,11 @@ describe('UserDetailsPage', () => { ...mockUserDetailsData, recentMilestones: [], } - ; (useQuery as jest.Mock).mockReturnValue({ - data: noMilestonesData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: noMilestonesData, + loading: false, + error: null, + }) render() await waitFor(() => { expect(screen.getByText('Recent Milestones')).toBeInTheDocument() @@ -486,11 +486,11 @@ describe('UserDetailsPage', () => { publicRepositoriesCount: 0, }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: zeroStatsData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: zeroStatsData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -511,11 +511,11 @@ describe('UserDetailsPage', () => { location: '', }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: minimalData, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: minimalData, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -526,7 +526,7 @@ describe('UserDetailsPage', () => { }) }) test('does not render sponsor block', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, error: null, }) @@ -538,7 +538,7 @@ describe('UserDetailsPage', () => { describe('Badge Display Tests', () => { test('renders badges section when user has badges', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -552,7 +552,7 @@ describe('UserDetailsPage', () => { }) test('renders badges with correct props', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -580,11 +580,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithoutBadges, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -602,11 +602,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithoutBadges, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithoutBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -631,11 +631,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithIncompleteBadges, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithIncompleteBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -661,35 +661,16 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery 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('renders multiple badges in correct order', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ - data: mockUserDetailsData, + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithEmptyCssClass, loading: false, error: null, }) render() await waitFor(() => { - const badges = screen.getAllByTestId(/^badge-/) - - expect(badges).toHaveLength(2) - // Security Expert has weight 2, should come first - expect(badges[0]).toHaveAttribute('data-testid', 'badge-security-expert') - // Contributor has weight 1, should come second - expect(badges[1]).toHaveAttribute('data-testid', 'badge-contributor') + const badge = screen.getByTestId('badge-test-badge') + expect(badge).toHaveAttribute('data-css-class', 'fa-medal') }) }) @@ -709,12 +690,11 @@ describe('UserDetailsPage', () => { ], }, } - - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithSpecialBadges, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithSpecialBadges, + loading: false, + error: null, + }) render() await waitFor(() => { @@ -739,11 +719,11 @@ describe('UserDetailsPage', () => { }, } - ; (useQuery as jest.Mock).mockReturnValue({ - data: dataWithLongNameBadge, - loading: false, - error: null, - }) + ;(useQuery as jest.Mock).mockReturnValue({ + data: dataWithLongNameBadge, + loading: false, + error: null, + }) render() await waitFor(() => { diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index bf773baddb..4a46fff46a 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -13,6 +13,7 @@ import { useTheme } from 'next-themes' import React, { useState, useEffect, useRef } from 'react' import { handleAppError, ErrorDisplay } from 'app/global-error' import { GET_USER_DATA } from 'server/queries/userQueries' +import { Badge } from 'types/badge' import type { Issue } from 'types/issue' import type { Milestone } from 'types/milestone' import type { RepositoryCardProps } from 'types/project' @@ -195,27 +196,25 @@ const UserDetailsPage: React.FC = () => { alt={user?.name || user?.login || 'User Avatar'} />
-
+
@{user?.login} {user?.badges && user.badges.length > 0 && (
- {user.badges - .slice() - .sort((a, b) => b.weight - a.weight) - .map((badge) => ( + {user.badges.slice().map((badge: Badge) => ( + - ))} + + ))}
)}
-

{formattedBio}

+

{formattedBio}

{!isPrivateContributor && (
diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx index 93a730889d..9b6a2364ea 100644 --- a/frontend/src/app/members/page.tsx +++ b/frontend/src/app/members/page.tsx @@ -34,7 +34,7 @@ const UsersPage = () => { onclick: () => handleButtonClick(user), } - const badgeCount = user.badges?.length || 0 + const badgeCount = user.badgeCount || 0 return ( { - if (cssClass == null) { + if (!cssClass || typeof cssClass !== 'string') { return (
- {showTooltip ? ( - - - - ) : ( - - )} -
- ) - } - if (typeof cssClass !== 'string') { - return ( -
- {showTooltip ? ( - - - - ) : ( - - )} -
- ) - } - - // Empty string should gracefully fall back to medal without error text - if (cssClass === '') { - return ( -
- {showTooltip ? ( - - - - ) : ( - - )} -
- ) - } - - const trimmed = String(cssClass).trim() - if (trimmed.length === 0) { - return ( -
- {showTooltip ? ( - - - - ) : ( - - )} + + +
) } - const tokens = trimmed.split(/\s+/) - const styleToken = tokens.find((t) => /^fa-(solid|regular|brands)$/i.test(t)) - const stylePrefix: IconLookup['prefix'] = - styleToken?.toLowerCase() === 'fa-regular' - ? 'far' - : styleToken?.toLowerCase() === 'fa-brands' - ? 'fab' - : 'fas' - - // Allow either a plain icon token (e.g., "crown") or an fa- prefixed token (e.g., "fa-crown") - const plainIconToken = tokens - .slice() - .reverse() - .find((t) => /^[a-z0-9-]+$/i.test(t)) - - const faIconToken = tokens - .slice() - .reverse() - .find((t) => /^fa-[a-z0-9-]+$/i.test(t) && !/^fa-(solid|regular|brands)$/i.test(t)) - - const chosenToken = plainIconToken || faIconToken || 'fa-medal' - const iconName = chosenToken.replace(/^fa-/, '') as IconName - - // Check if the icon exists in the FA library for the derived style - const lookup: IconLookup = { prefix: stylePrefix, iconName } - let iconFound = false - try { - findIconDefinition(lookup) - iconFound = true - } catch { - iconFound = false - } - - if (!iconFound) { + const iconDef = findIconDefinition({ + prefix: 'fas', + iconName: cssClass.split(' ').pop().replace('fa-', '') as IconName, + }) + if (!iconDef) { return (
- {showTooltip ? ( - - - - ) : ( - - )} + + +
) } - return (
- {showTooltip ? ( - - - - ) : ( - - )} + + +
) } diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts index 6bf165686f..ba05f2ae4a 100644 --- a/frontend/src/types/__generated__/graphql.ts +++ b/frontend/src/types/__generated__/graphql.ts @@ -37,7 +37,7 @@ export type AuthUserNode = Node & { username: Scalars['String']['output']; }; -export type BadgeNode = { +export type BadgeNode = Node & { __typename?: 'BadgeNode'; cssClass: Scalars['String']['output']; description: Scalars['String']['output']; From ac27f2612e6ce96496d25cb5e1aa93f6a15a5868 Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Thu, 25 Sep 2025 17:15:03 +0530 Subject: [PATCH 26/37] done with changes suggested by kate . --- frontend/src/components/Badges.tsx | 12 ++---------- frontend/src/components/UserCard.tsx | 2 +- frontend/src/server/queries/userQueries.ts | 2 -- .../src/types/__generated__/userQueries.generated.ts | 8 ++++---- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 5851f4ffa5..584eab42aa 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -23,19 +23,11 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { prefix: 'fas', iconName: cssClass.split(' ').pop().replace('fa-', '') as IconName, }) - if (!iconDef) { - return ( -
- - - -
- ) - } + return (
- +
) diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx index 348d30bb16..34ed8b1f77 100644 --- a/frontend/src/components/UserCard.tsx +++ b/frontend/src/components/UserCard.tsx @@ -13,8 +13,8 @@ import type { UserCardProps } from 'types/card' const UserCard = ({ avatar, - button, badgeCount, + button, className, company, description, diff --git a/frontend/src/server/queries/userQueries.ts b/frontend/src/server/queries/userQueries.ts index 8c14d65137..8a363ab9f8 100644 --- a/frontend/src/server/queries/userQueries.ts +++ b/frontend/src/server/queries/userQueries.ts @@ -12,7 +12,6 @@ export const GET_LEADER_DATA = gql` name description cssClass - weight } badgeCount } @@ -91,7 +90,6 @@ export const GET_USER_DATA = gql` description id name - weight } badgeCount publicRepositoriesCount diff --git a/frontend/src/types/__generated__/userQueries.generated.ts b/frontend/src/types/__generated__/userQueries.generated.ts index 2cdf040c3a..13f1f898c2 100644 --- a/frontend/src/types/__generated__/userQueries.generated.ts +++ b/frontend/src/types/__generated__/userQueries.generated.ts @@ -6,14 +6,14 @@ export type GetLeaderDataQueryVariables = Types.Exact<{ }>; -export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, badgeCount: number, badges: Array<{ __typename: 'BadgeNode', id: string, name: string, description: string, cssClass: string, weight: number }> } | null }; +export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, badgeCount: number, badges: Array<{ __typename: 'BadgeNode', id: string, name: string, description: string, cssClass: string }> } | null }; export type GetUserDataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; }>; -export type GetUserDataQuery = { recentIssues: Array<{ __typename: 'IssueNode', id: string, createdAt: unknown, 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: unknown, url: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: unknown, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, publishedAt: unknown | 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, badgeCount: number, 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 GetUserDataQuery = { recentIssues: Array<{ __typename: 'IssueNode', id: string, createdAt: unknown, 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: unknown, url: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: unknown, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, publishedAt: unknown | 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, badgeCount: number, publicRepositoriesCount: number, releasesCount: number, updatedAt: number, url: string, badges: Array<{ __typename: 'BadgeNode', cssClass: string, description: string, id: string, name: string }> } | null }; export type GetUserMetadataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; @@ -23,6 +23,6 @@ export type GetUserMetadataQueryVariables = Types.Exact<{ export type GetUserMetadataQuery = { user: { __typename: 'UserNode', id: string, bio: string, login: string, name: string, badgeCount: number } | 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"}},{"kind":"Field","name":{"kind":"Name","value":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cssClass"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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":"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":"badgeCount"}},{"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 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":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cssClass"}}]}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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":"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":"badgeCount"}},{"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"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file From 7535bc985e72627e64f98f656d7a0844198f1475 Mon Sep 17 00:00:00 2001 From: PIYUSH RATHORE <163632958+Piyushrathoree@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:25:37 +0530 Subject: [PATCH 27/37] Update frontend/src/components/Badges.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- frontend/src/components/Badges.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 584eab42aa..721d155f7d 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -27,10 +27,15 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { return (
- +
) +
+ ) } export default Badges From fb1e4034715eba8544cb3bd9d4490746dcd8117f Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Fri, 26 Sep 2025 13:07:10 +0530 Subject: [PATCH 28/37] CodeRabbit: For Font Awesome icons, the CSS classes are backend-driven (e.g., fa-crown for fa-solid). The frontend dynamically extracts the correct prefix for lookup and rendering. This is a critical design for backend compatibility,they are only fa-solid icons so badges.tsx only checks for fa-solid not anyother families. fix the coderabbit issue with syntax. also run make check-test --- frontend/src/components/Badges.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 721d155f7d..793bab05b3 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -13,7 +13,7 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { return (
- +
) @@ -28,14 +28,12 @@ const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => {
) -
- ) } export default Badges From f39dec0330c935d5762b249c472d88590e1191b8 Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Thu, 9 Oct 2025 08:26:16 +0530 Subject: [PATCH 29/37] migration --- .../migrations/0007_alter_badge_css_class.py | 28 ++++++ backend/apps/nest/models/badge.py | 11 ++- .../__tests__/unit/components/Badges.test.tsx | 88 +++++++++++++++---- frontend/src/components/Badges.tsx | 29 +++--- frontend/src/utils/data.ts | 19 ++++ 5 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 backend/apps/nest/migrations/0007_alter_badge_css_class.py 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..6e07b064c9 --- /dev/null +++ b/backend/apps/nest/migrations/0007_alter_badge_css_class.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.6 on 2025-10-08 16:47 +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..f18414623f 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" + MEDAL = "medal", "Medal" + RIBBON = "ribbon", "Ribbon" + STAR = "star", "Star" + CERTIFICATE = "certificate", "Certificate" + BUG_SLASH = "bug_slash", "Bug Slash" + 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/frontend/__tests__/unit/components/Badges.test.tsx b/frontend/__tests__/unit/components/Badges.test.tsx index f05b0e1d37..1878dcbb89 100644 --- a/frontend/__tests__/unit/components/Badges.test.tsx +++ b/frontend/__tests__/unit/components/Badges.test.tsx @@ -2,18 +2,42 @@ import { render, screen } from '@testing-library/react' import React from 'react' import Badges from 'components/Badges' -// Mock FontAwesome -jest.mock('@fortawesome/fontawesome-svg-core', () => ({ - findIconDefinition: jest.fn(({ iconName }: { iconName: string }) => { - // Return truthy value for valid icons, null for invalid - return iconName === 'medal' ? { iconName } : null - }), -})) +jest.mock('@fortawesome/fontawesome-svg-core', () => { + const { BADGE_CLASS_MAP } = jest.requireActual('utils/data') + const registered = new Set( + Object.values(BADGE_CLASS_MAP).map((s: string) => s.split(' ').pop()?.replace('fa-', '')) + ) + + return { + findIconDefinition: jest.fn(({ iconName }: { iconName: string }) => { + return registered.has(iconName) ? { iconName } : null + }), + } +}) jest.mock('wrappers/FontAwesomeIconWrapper', () => { - return function MockFontAwesomeIconWrapper({ icon }: { icon: string }) { - const iconName = icon.split(' ').pop()?.replace('fa-', '') || icon.replace('fa-', '') - return + const RealWrapper = jest.requireActual('wrappers/FontAwesomeIconWrapper').default + + const getName = (icon: any) => { + 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: any) { + const name = getName(props.icon) + return ( +
+ +
+ ) } }) @@ -38,35 +62,65 @@ jest.mock('@heroui/tooltip', () => ({ }, })) +//only for confirming the badges are working properly describe('Badges Component', () => { const defaultProps = { name: 'Test Badge', - cssClass: 'fa-solid fa-medal', + cssClass: 'medal', } it('renders valid icon with tooltip', () => { render() - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + 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() - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + const icon = screen.getByTestId('badge-icon') + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('data-icon', 'medal') }) - it('renders fa-question fallback for unrecognized icon', () => { - render() + it('renders fallback medal for unrecognized icon', () => { + render() - expect(screen.getByTestId('icon-question')).toBeInTheDocument() + const icon = screen.getByTestId('badge-icon') + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('data-icon', 'medal') }) it('hides tooltip when showTooltip is false', () => { render() - expect(screen.getByTestId('icon-medal')).toBeInTheDocument() + 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' }, + ] + + backendIcons.forEach(({ cssClass, expectedIcon }) => { + it(`renders ${cssClass} icon correctly`, () => { + render() + + const icon = screen.getByTestId('badge-icon') + expect(icon).toBeInTheDocument() + expect(icon).toHaveAttribute('data-icon', expectedIcon) + }) + }) + }) }) diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 793bab05b3..bd14cb180c 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -1,6 +1,7 @@ -import { IconName, findIconDefinition } from '@fortawesome/fontawesome-svg-core' +import { findIconDefinition } from '@fortawesome/fontawesome-svg-core' import { Tooltip } from '@heroui/tooltip' import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' +import { BADGE_CLASS_MAP } from 'utils/data' type BadgeProps = { name: string @@ -8,29 +9,19 @@ type BadgeProps = { showTooltip?: boolean } -const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { - if (!cssClass || typeof cssClass !== 'string') { - return ( -
- - - -
- ) - } +const DEFAULT = BADGE_CLASS_MAP['medal'] - const iconDef = findIconDefinition({ - prefix: 'fas', - iconName: cssClass.split(' ').pop().replace('fa-', '') as IconName, - }) +const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { + const cls = typeof cssClass === 'string' ? BADGE_CLASS_MAP[cssClass] : undefined + const icon = cls ?? DEFAULT + const iconName = icon.split(' ').pop()?.replace('fa-', '') || 'medal' + const def = findIconDefinition({ prefix: 'fas', iconName: iconName as any }) + const iconClass = def ? icon : DEFAULT return (
- +
) diff --git a/frontend/src/utils/data.ts b/frontend/src/utils/data.ts index 1be07c97ff..9462f7aa48 100644 --- a/frontend/src/utils/data.ts +++ b/frontend/src/utils/data.ts @@ -31,12 +31,22 @@ import { faX, faPeopleGroup, faSun, + faAward, + faMedal, + faRibbon, + faCertificate, + faBug } from '@fortawesome/free-solid-svg-icons' library.add( faArrowsRotate, faCodeFork, faStar, + faMedal, + faAward, + faRibbon, + faCertificate, + faBug, faUser, faClock, faComment, @@ -63,6 +73,15 @@ library.add( faSun ) +export const BADGE_CLASS_MAP: Record = { + award: faAward, + medal: faMedal, + ribbon: faRibbon, + star: faStar, + certificate: faCertificate, + bug_slash: faBug, +} as const + export const ICONS = { starsCount: { label: 'GitHub stars', From 878059dade7573c60c10c56c79fe1adb19fd7d3a Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Thu, 9 Oct 2025 10:07:19 +0530 Subject: [PATCH 30/37] Update migration timestamp, enhance badge component, and import icon type definition --- .../nest/migrations/0007_alter_badge_css_class.py | 3 ++- backend/apps/nest/models/badge.py | 2 +- frontend/src/app/members/[memberKey]/page.tsx | 3 ++- frontend/src/components/Badges.tsx | 15 +++++++-------- frontend/src/utils/data.ts | 1 + 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/apps/nest/migrations/0007_alter_badge_css_class.py b/backend/apps/nest/migrations/0007_alter_badge_css_class.py index 6e07b064c9..fdb7ad07d7 100644 --- a/backend/apps/nest/migrations/0007_alter_badge_css_class.py +++ b/backend/apps/nest/migrations/0007_alter_badge_css_class.py @@ -1,4 +1,5 @@ -# Generated by Django 5.2.6 on 2025-10-08 16:47 +# Generated by Django 5.2.6 on 2025-10-09 04:06 + from django.db import migrations, models diff --git a/backend/apps/nest/models/badge.py b/backend/apps/nest/models/badge.py index f18414623f..fc343abb4d 100644 --- a/backend/apps/nest/models/badge.py +++ b/backend/apps/nest/models/badge.py @@ -27,7 +27,7 @@ class Meta: verbose_name="CSS Class", max_length=255, choices=BadgeCssClass.choices, - default=BadgeCssClass.MEDAL + default=BadgeCssClass.MEDAL, ) description = models.CharField( verbose_name="Description", diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index a65240606d..6ddc489556 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -12,7 +12,7 @@ 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 { GET_USER_DATA } from 'server/queries/userQueries' + import { Badge } from 'types/badge' import type { Issue } from 'types/issue' import type { Milestone } from 'types/milestone' @@ -25,6 +25,7 @@ import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/ import Badges from 'components/Badges' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' +import { GetUserDataDocument } from 'types/__generated__/userQueries.generated' const UserDetailsPage: React.FC = () => { const { memberKey } = useParams<{ memberKey: string }>() diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index bd14cb180c..c4546fbafe 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -1,4 +1,3 @@ -import { findIconDefinition } from '@fortawesome/fontawesome-svg-core' import { Tooltip } from '@heroui/tooltip' import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' import { BADGE_CLASS_MAP } from 'utils/data' @@ -9,19 +8,19 @@ type BadgeProps = { showTooltip?: boolean } -const DEFAULT = BADGE_CLASS_MAP['medal'] +const DEFAULT_ICON = BADGE_CLASS_MAP['medal'] + +const resolveIcon = (cssClass: string) => { + return BADGE_CLASS_MAP[cssClass] ?? DEFAULT_ICON +} const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { - const cls = typeof cssClass === 'string' ? BADGE_CLASS_MAP[cssClass] : undefined - const icon = cls ?? DEFAULT - const iconName = icon.split(' ').pop()?.replace('fa-', '') || 'medal' - const def = findIconDefinition({ prefix: 'fas', iconName: iconName as any }) - const iconClass = def ? icon : DEFAULT + const icon = resolveIcon(cssClass) return (
- +
) diff --git a/frontend/src/utils/data.ts b/frontend/src/utils/data.ts index 9462f7aa48..a848474d7b 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, From 6e1bbe755974f19d25bcd9734a892b739fad0dd1 Mon Sep 17 00:00:00 2001 From: Piyushrathoree Date: Thu, 9 Oct 2025 10:12:47 +0530 Subject: [PATCH 31/37] Refactor badge class key from 'bug_slash' to 'bugSlash' for consistency --- frontend/src/app/members/[memberKey]/page.tsx | 2 +- frontend/src/utils/data.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 6ddc489556..396f89658e 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -13,6 +13,7 @@ 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' @@ -25,7 +26,6 @@ import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/ import Badges from 'components/Badges' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' -import { GetUserDataDocument } from 'types/__generated__/userQueries.generated' const UserDetailsPage: React.FC = () => { const { memberKey } = useParams<{ memberKey: string }>() diff --git a/frontend/src/utils/data.ts b/frontend/src/utils/data.ts index a848474d7b..28633701f9 100644 --- a/frontend/src/utils/data.ts +++ b/frontend/src/utils/data.ts @@ -80,7 +80,7 @@ export const BADGE_CLASS_MAP: Record = { ribbon: faRibbon, star: faStar, certificate: faCertificate, - bug_slash: faBug, + bugSlash: faBug, } as const export const ICONS = { From 848ae72b334ecdc780112966e9cc99b3f549e6ad Mon Sep 17 00:00:00 2001 From: Kate Date: Sun, 12 Oct 2025 17:55:18 -0700 Subject: [PATCH 32/37] Fix issues with badges --- backend/apps/core/utils/index.py | 1 + backend/apps/github/index/registry/user.py | 1 + backend/apps/github/index/search/user.py | 1 + backend/apps/github/models/mixins/user.py | 5 +++ .../__tests__/unit/components/Badges.test.tsx | 29 ++++++------- .../__tests__/unit/pages/UserDetails.test.tsx | 16 ++++---- frontend/src/app/members/[memberKey]/page.tsx | 41 +++++++++++-------- frontend/src/app/members/page.tsx | 2 - frontend/src/components/Badges.tsx | 8 +++- frontend/src/server/queries/userQueries.ts | 2 + .../__generated__/userQueries.generated.ts | 10 ++--- frontend/src/utils/data.ts | 7 ++-- 12 files changed, 69 insertions(+), 54 deletions(-) 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/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..8ae3e4940c 100644 --- a/backend/apps/github/models/mixins/user.py +++ b/backend/apps/github/models/mixins/user.py @@ -182,3 +182,8 @@ def idx_updated_at(self) -> float: def idx_url(self) -> str: """Return GitHub profile URL for indexing.""" return self.url + + @property + def idx_badge_count(self) -> int: + """Return badge count for indexing.""" + return self.user_badges.filter(is_active=True).count() diff --git a/frontend/__tests__/unit/components/Badges.test.tsx b/frontend/__tests__/unit/components/Badges.test.tsx index 1878dcbb89..bcec77c5d0 100644 --- a/frontend/__tests__/unit/components/Badges.test.tsx +++ b/frontend/__tests__/unit/components/Badges.test.tsx @@ -2,23 +2,10 @@ import { render, screen } from '@testing-library/react' import React from 'react' import Badges from 'components/Badges' -jest.mock('@fortawesome/fontawesome-svg-core', () => { - const { BADGE_CLASS_MAP } = jest.requireActual('utils/data') - const registered = new Set( - Object.values(BADGE_CLASS_MAP).map((s: string) => s.split(' ').pop()?.replace('fa-', '')) - ) - - return { - findIconDefinition: jest.fn(({ iconName }: { iconName: string }) => { - return registered.has(iconName) ? { iconName } : null - }), - } -}) - jest.mock('wrappers/FontAwesomeIconWrapper', () => { const RealWrapper = jest.requireActual('wrappers/FontAwesomeIconWrapper').default - const getName = (icon: any) => { + const getName = (icon) => { if (!icon) return 'medal' if (typeof icon === 'string') { const m = icon.match(/fa-([a-z0-9-]+)$/i) @@ -31,7 +18,7 @@ jest.mock('wrappers/FontAwesomeIconWrapper', () => { return 'medal' } - return function MockFontAwesomeIconWrapper(props: any) { + return function MockFontAwesomeIconWrapper(props) { const name = getName(props.icon) return (
@@ -110,11 +97,11 @@ describe('Badges Component', () => { { cssClass: 'ribbon', expectedIcon: 'ribbon' }, { cssClass: 'star', expectedIcon: 'star' }, { cssClass: 'certificate', expectedIcon: 'certificate' }, - { cssClass: 'bug-slash', expectedIcon: 'bug' }, + { cssClass: 'bug_slash', expectedIcon: 'bug' }, // Backend snake_case input ] backendIcons.forEach(({ cssClass, expectedIcon }) => { - it(`renders ${cssClass} icon correctly`, () => { + it(`renders ${cssClass} icon correctly (transforms snake_case to camelCase)`, () => { render() const icon = screen.getByTestId('badge-icon') @@ -122,5 +109,13 @@ describe('Badges Component', () => { 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/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 664142ee32..7bbfb393cf 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -538,7 +538,7 @@ describe('UserDetailsPage', () => { describe('Badge Display Tests', () => { test('renders badges section when user has badges', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -552,7 +552,7 @@ describe('UserDetailsPage', () => { }) test('renders badges with correct props', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: mockUserDetailsData, loading: false, error: null, @@ -580,7 +580,7 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: dataWithoutBadges, loading: false, error: null, @@ -602,7 +602,7 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: dataWithoutBadges, loading: false, error: null, @@ -631,7 +631,7 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: dataWithIncompleteBadges, loading: false, error: null, @@ -661,7 +661,7 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: dataWithEmptyCssClass, loading: false, error: null, @@ -690,7 +690,7 @@ describe('UserDetailsPage', () => { ], }, } - ;(useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: dataWithSpecialBadges, loading: false, error: null, @@ -719,7 +719,7 @@ describe('UserDetailsPage', () => { }, } - ;(useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: dataWithLongNameBadge, loading: false, error: null, diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 396f89658e..5936f9609f 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -197,25 +197,30 @@ const UserDetailsPage: React.FC = () => { alt={user?.name || user?.login || 'User Avatar'} />
-
- - @{user?.login} - - {user?.badges && user.badges.length > 0 && ( -
- {user.badges.slice().map((badge: Badge) => ( - - - - ))} -
- )} +
+
+ + @{user?.login} + + {user?.badges && user.badges.length > 0 && ( +
+ {user.badges.slice().map((badge: Badge) => ( + + + + ))} +
+ )} +
+

{formattedBio}

-

{formattedBio}

{!isPrivateContributor && (
diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx index 9b6a2364ea..a2a8808577 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}`) } @@ -35,7 +34,6 @@ const UsersPage = () => { } const badgeCount = user.badgeCount || 0 - return ( { + // Convert backend snake_case format to frontend camelCase format + return cssClass.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) +} + const resolveIcon = (cssClass: string) => { - return BADGE_CLASS_MAP[cssClass] ?? DEFAULT_ICON + const normalizedClass = normalizeCssClass(cssClass) + return BADGE_CLASS_MAP[normalizedClass] ?? DEFAULT_ICON } const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => { diff --git a/frontend/src/server/queries/userQueries.ts b/frontend/src/server/queries/userQueries.ts index d12a5e1a02..7a880e8699 100644 --- a/frontend/src/server/queries/userQueries.ts +++ b/frontend/src/server/queries/userQueries.ts @@ -14,6 +14,7 @@ export const GET_LEADER_DATA = gql` name description cssClass + weight } badgeCount } @@ -92,6 +93,7 @@ export const GET_USER_DATA = gql` description id name + weight } badgeCount publicRepositoriesCount diff --git a/frontend/src/types/__generated__/userQueries.generated.ts b/frontend/src/types/__generated__/userQueries.generated.ts index 95bdec8dbf..3db58ad532 100644 --- a/frontend/src/types/__generated__/userQueries.generated.ts +++ b/frontend/src/types/__generated__/userQueries.generated.ts @@ -6,14 +6,14 @@ export type GetLeaderDataQueryVariables = Types.Exact<{ }>; -export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, badgeCount: number, badges: Array<{ __typename: 'BadgeNode', id: string, name: string, description: string, cssClass: string }> } | null }; +export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, company: string, location: string, login: string, name: string, badgeCount: number, badges: Array<{ __typename: 'BadgeNode', id: string, name: string, description: string, cssClass: string, weight: number }> } | null }; export type GetUserDataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; }>; -export type GetUserDataQuery = { recentIssues: Array<{ __typename: 'IssueNode', id: string, createdAt: unknown, 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: unknown, url: string }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', id: string, createdAt: unknown, organizationName: string | null, repositoryName: string | null, title: string, url: string }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, isPreRelease: boolean, name: string, publishedAt: unknown | 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, badgeCount: number, publicRepositoriesCount: number, releasesCount: number, updatedAt: number, url: string, badges: Array<{ __typename: 'BadgeNode', cssClass: string, description: string, id: string, name: 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', 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, badgeCount: number, 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']; @@ -23,6 +23,6 @@ export type GetUserMetadataQueryVariables = Types.Exact<{ export type GetUserMetadataQuery = { user: { __typename: 'UserNode', id: string, bio: string, login: string, name: string, badgeCount: number } | 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"}},{"kind":"Field","name":{"kind":"Name","value":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cssClass"}}]}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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":"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":"badgeCount"}},{"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"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} as unknown as DocumentNode; +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":"company"}},{"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":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cssClass"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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":"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":"badgeCount"}},{"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"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/utils/data.ts b/frontend/src/utils/data.ts index 28633701f9..bf2dd4402f 100644 --- a/frontend/src/utils/data.ts +++ b/frontend/src/utils/data.ts @@ -36,7 +36,8 @@ import { faMedal, faRibbon, faCertificate, - faBug + faBug, + faStar as fasStar, } from '@fortawesome/free-solid-svg-icons' library.add( @@ -71,14 +72,14 @@ library.add( faSlack, faPeopleGroup, faXTwitter, - faSun + faSun, ) export const BADGE_CLASS_MAP: Record = { award: faAward, medal: faMedal, ribbon: faRibbon, - star: faStar, + star: fasStar, certificate: faCertificate, bugSlash: faBug, } as const From 142786e5c0681397d3993ae0fb0ebc957b63e33a Mon Sep 17 00:00:00 2001 From: Kate Date: Sun, 12 Oct 2025 18:01:56 -0700 Subject: [PATCH 33/37] Fix make check --- frontend/src/types/__generated__/userQueries.generated.ts | 7 +++---- frontend/src/utils/data.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/types/__generated__/userQueries.generated.ts b/frontend/src/types/__generated__/userQueries.generated.ts index 5bc8aabbc4..01048b73b4 100644 --- a/frontend/src/types/__generated__/userQueries.generated.ts +++ b/frontend/src/types/__generated__/userQueries.generated.ts @@ -6,7 +6,7 @@ export type GetLeaderDataQueryVariables = Types.Exact<{ }>; -export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, company: string, location: string, login: string, name: string, badgeCount: number, badges: Array<{ __typename: 'BadgeNode', id: string, name: string, description: string, cssClass: string, weight: number }> } | null }; +export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, badgeCount: number, badges: Array<{ __typename: 'BadgeNode', id: string, name: string, description: string, cssClass: string, weight: number }> } | null }; export type GetUserDataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; @@ -23,7 +23,6 @@ export type GetUserMetadataQueryVariables = Types.Exact<{ export type GetUserMetadataQuery = { user: { __typename: 'UserNode', id: string, bio: string, login: string, name: string, badgeCount: number } | 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":"company"}},{"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":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cssClass"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} as unknown as DocumentNode; +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":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cssClass"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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":"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":"badgeCount"}},{"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"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/utils/data.ts b/frontend/src/utils/data.ts index bf2dd4402f..d77d47e83b 100644 --- a/frontend/src/utils/data.ts +++ b/frontend/src/utils/data.ts @@ -72,7 +72,7 @@ library.add( faSlack, faPeopleGroup, faXTwitter, - faSun, + faSun ) export const BADGE_CLASS_MAP: Record = { From 2078a7064195b7e7e0d34f7936efa8ba5ae96938 Mon Sep 17 00:00:00 2001 From: Kate Date: Sun, 12 Oct 2025 18:03:58 -0700 Subject: [PATCH 34/37] Guard against null/undefined cssClass in normalizeCssClass --- frontend/src/components/Badges.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Badges.tsx b/frontend/src/components/Badges.tsx index 680df0552f..d6c3151392 100644 --- a/frontend/src/components/Badges.tsx +++ b/frontend/src/components/Badges.tsx @@ -4,18 +4,22 @@ import { BADGE_CLASS_MAP } from 'utils/data' type BadgeProps = { name: string - cssClass: string + cssClass: string | undefined showTooltip?: boolean } const DEFAULT_ICON = BADGE_CLASS_MAP['medal'] -const normalizeCssClass = (cssClass: string) => { +const normalizeCssClass = (cssClass: string | undefined) => { + if (!cssClass || cssClass.trim() === '') { + return '' + } + // Convert backend snake_case format to frontend camelCase format - return cssClass.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) + return cssClass.trim().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) } -const resolveIcon = (cssClass: string) => { +const resolveIcon = (cssClass: string | undefined) => { const normalizedClass = normalizeCssClass(cssClass) return BADGE_CLASS_MAP[normalizedClass] ?? DEFAULT_ICON } From 84498fc9aca8f6910dd1a992a7b11dcb6e3604f5 Mon Sep 17 00:00:00 2001 From: Kate Date: Sun, 12 Oct 2025 18:18:57 -0700 Subject: [PATCH 35/37] Add test to verify badge rendering order per backend contract --- .../apps/github/api/internal/nodes/user.py | 2 +- .../github/api/internal/nodes/user_test.py | 16 ++-- .../__tests__/unit/pages/UserDetails.test.tsx | 73 +++++++++++++++++++ 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index 228929d616..e6c9ac9f3e 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -32,7 +32,7 @@ class UserNode: def badges(self) -> list[BadgeNode]: """List badges assigned to the user sorted by weight and name.""" user_badges = self.user_badges.select_related("badge").order_by( - "-badge__weight", "badge__name" + "badge__weight", "badge__name" ) return [user_badge.badge for user_badge in user_badges] 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 72d77a7d73..393709302b 100644 --- a/backend/tests/apps/github/api/internal/nodes/user_test.py +++ b/backend/tests/apps/github/api/internal/nodes/user_test.py @@ -107,7 +107,7 @@ def test_badges_field_empty(self): assert result == [] mock_badges_queryset.select_related.assert_called_once_with("badge") mock_badges_queryset.select_related.return_value.order_by.assert_called_once_with( - "-badge__weight", "badge__name" + "badge__weight", "badge__name" ) def test_badges_field_single_badge(self): @@ -125,7 +125,7 @@ def test_badges_field_single_badge(self): assert result == [mock_badge] mock_badges_queryset.select_related.assert_called_once_with("badge") mock_badges_queryset.select_related.return_value.order_by.assert_called_once_with( - "-badge__weight", "badge__name" + "badge__weight", "badge__name" ) def test_badges_field_sorted_by_weight_and_name(self): @@ -161,13 +161,13 @@ def test_badges_field_sorted_by_weight_and_name(self): mock_user_badge_low.badge = mock_badge_low_weight # Set up the mock queryset to return badges in the expected sorted order - # (highest weight first, then by name for same weight) + # (lowest weight first, then by name for same weight) mock_badges_queryset = Mock() mock_badges_queryset.select_related.return_value.order_by.return_value = [ - mock_user_badge_high, # weight 100 + 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_low, # weight 10 + mock_user_badge_high, # weight 100 ] mock_user = Mock() mock_user.user_badges = mock_badges_queryset @@ -176,15 +176,15 @@ def test_badges_field_sorted_by_weight_and_name(self): # Verify the badges are returned in the correct order expected_badges = [ - mock_badge_high_weight, + mock_badge_low_weight, mock_badge_medium_weight_a, mock_badge_medium_weight_b, - mock_badge_low_weight, + mock_badge_high_weight, ] assert result == expected_badges # Verify the queryset was called with correct ordering mock_badges_queryset.select_related.assert_called_once_with("badge") mock_badges_queryset.select_related.return_value.order_by.assert_called_once_with( - "-badge__weight", "badge__name" + "badge__weight", "badge__name" ) diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 7bbfb393cf..89498478f5 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -732,5 +732,78 @@ describe('UserDetailsPage', () => { ).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) + }) + }) }) }) From bf7e1122ce149b50fbbe3f1e1732a61ecc2347c7 Mon Sep 17 00:00:00 2001 From: Kate Date: Sun, 12 Oct 2025 18:24:52 -0700 Subject: [PATCH 36/37] Fix backend test for getting users --- backend/tests/apps/core/utils/match_test.py | 1 + 1 file changed, 1 insertion(+) 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", From 6c2a612e17fa18cc463ce3c6efa47edb8d29d35b Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Sun, 12 Oct 2025 21:04:07 -0700 Subject: [PATCH 37/37] Update code --- .../apps/github/api/internal/nodes/user.py | 25 ++- backend/apps/github/models/mixins/user.py | 10 +- backend/apps/nest/models/badge.py | 4 +- .../github/api/internal/nodes/user_test.py | 40 ++--- cspell/custom-dict.txt | 6 - frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 156 +++++++++--------- frontend/src/app/members/page.tsx | 4 +- frontend/src/server/queries/userQueries.ts | 28 ++-- .../__generated__/userQueries.generated.ts | 12 +- frontend/src/types/badge.ts | 4 +- frontend/src/types/card.ts | 2 +- frontend/src/types/user.ts | 4 +- frontend/src/utils/data.ts | 64 +++---- 14 files changed, 183 insertions(+), 178 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index e6c9ac9f3e..f9fbb7198e 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -28,19 +28,28 @@ 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]: - """List badges assigned to the user sorted by weight and name.""" - user_badges = self.user_badges.select_related("badge").order_by( - "badge__weight", "badge__name" + """Return user badges.""" + user_badges = ( + self.user_badges.filter( + is_active=True, + ) + .select_related( + "badge", + ) + .order_by( + "badge__weight", + "badge__name", + ) ) return [user_badge.badge for user_badge in user_badges] - @strawberry.field - def badge_count(self) -> int: - """Resolve badge count.""" - return self.user_badges.values("badge_id").distinct().count() - @strawberry.field def created_at(self) -> float: """Resolve created at.""" diff --git a/backend/apps/github/models/mixins/user.py b/backend/apps/github/models/mixins/user.py index 8ae3e4940c..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.""" @@ -182,8 +187,3 @@ def idx_updated_at(self) -> float: def idx_url(self) -> str: """Return GitHub profile URL for indexing.""" return self.url - - @property - def idx_badge_count(self) -> int: - """Return badge count for indexing.""" - return self.user_badges.filter(is_active=True).count() diff --git a/backend/apps/nest/models/badge.py b/backend/apps/nest/models/badge.py index fc343abb4d..112b3d1575 100644 --- a/backend/apps/nest/models/badge.py +++ b/backend/apps/nest/models/badge.py @@ -12,11 +12,11 @@ class Badge(BulkSaveModel, TimestampedModel): 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" - CERTIFICATE = "certificate", "Certificate" - BUG_SLASH = "bug_slash", "Bug Slash" class Meta: db_table = "nest_badges" 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 393709302b..8da4ba1943 100644 --- a/backend/tests/apps/github/api/internal/nodes/user_test.py +++ b/backend/tests/apps/github/api/internal/nodes/user_test.py @@ -87,28 +87,28 @@ def test_badge_count_field(self): """Test badge_count field resolution.""" mock_user = Mock() mock_badges_queryset = Mock() - mock_badges_queryset.values.return_value.distinct.return_value.count.return_value = 3 + mock_badges_queryset.filter.return_value.count.return_value = 3 mock_user.user_badges = mock_badges_queryset result = UserNode.badge_count(mock_user) assert result == 3 - mock_badges_queryset.values.assert_called_once_with("badge_id") - mock_badges_queryset.values.return_value.distinct.assert_called_once() - mock_badges_queryset.values.return_value.distinct.return_value.count.assert_called_once() + mock_badges_queryset.filter.assert_called_once_with(is_active=True) + mock_badges_queryset.filter.return_value.count.assert_called_once() def test_badges_field_empty(self): """Test badges field resolution with no badges.""" mock_user = Mock() mock_badges_queryset = Mock() - mock_badges_queryset.select_related.return_value.order_by.return_value = [] + 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 result = UserNode.badges(mock_user) assert result == [] - mock_badges_queryset.select_related.assert_called_once_with("badge") - mock_badges_queryset.select_related.return_value.order_by.assert_called_once_with( - "badge__weight", "badge__name" - ) + 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_single_badge(self): """Test badges field resolution with single badge.""" @@ -118,15 +118,16 @@ def test_badges_field_single_badge(self): mock_user_badge.badge = mock_badge mock_badges_queryset = Mock() - mock_badges_queryset.select_related.return_value.order_by.return_value = [mock_user_badge] + 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.select_related.assert_called_once_with("badge") - mock_badges_queryset.select_related.return_value.order_by.assert_called_once_with( - "badge__weight", "badge__name" - ) + 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.""" @@ -163,7 +164,9 @@ def test_badges_field_sorted_by_weight_and_name(self): # 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_badges_queryset.select_related.return_value.order_by.return_value = [ + 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" @@ -184,7 +187,6 @@ def test_badges_field_sorted_by_weight_and_name(self): assert result == expected_badges # Verify the queryset was called with correct ordering - mock_badges_queryset.select_related.assert_called_once_with("badge") - mock_badges_queryset.select_related.return_value.order_by.assert_called_once_with( - "badge__weight", "badge__name" - ) + 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/cspell/custom-dict.txt b/cspell/custom-dict.txt index 3704c8de2d..9a191d1b4a 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -56,13 +56,7 @@ demojize dismissable dsn env -fa-brands -fa-regular -fa-solid -fab facebookexternalhit -far -fas gamesec geocoders geoloc 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/page.tsx b/frontend/src/app/members/page.tsx index a2a8808577..88ca788835 100644 --- a/frontend/src/app/members/page.tsx +++ b/frontend/src/app/members/page.tsx @@ -37,10 +37,10 @@ const UsersPage = () => { return ( ; -export type GetLeaderDataQuery = { user: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, badgeCount: number, badges: Array<{ __typename: 'BadgeNode', id: string, name: string, description: string, cssClass: string, weight: number }> } | 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, badgeCount: number, 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 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, badgeCount: number } | 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"}},{"kind":"Field","name":{"kind":"Name","value":"badges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"cssClass"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}}]}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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":"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":"badgeCount"}},{"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"}},{"kind":"Field","name":{"kind":"Name","value":"badgeCount"}}]}}]}}]} 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 index ca50a8872f..9fb95f689a 100644 --- a/frontend/src/types/badge.ts +++ b/frontend/src/types/badge.ts @@ -1,7 +1,7 @@ export type Badge = { - readonly id: string - readonly name: string 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 d8414d0d09..8e9125989e 100644 --- a/frontend/src/types/card.ts +++ b/frontend/src/types/card.ts @@ -79,8 +79,8 @@ export interface DetailsCardProps { export interface UserCardProps { avatar: string - badges?: Badge[] 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 3e292882d7..293933f89f 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -10,9 +10,9 @@ export type RepositoryDetails = { export type User = { avatarUrl: string - bio?: string - badges?: Badge[] badgeCount?: number + badges?: Badge[] + bio?: string company?: string contributionsCount?: number createdAt?: number | string diff --git a/frontend/src/utils/data.ts b/frontend/src/utils/data.ts index d77d47e83b..837ac1d829 100644 --- a/frontend/src/utils/data.ts +++ b/frontend/src/utils/data.ts @@ -7,8 +7,8 @@ import { faLinkedin, faMeetup, faSlack, - faYoutube, faXTwitter, + faYoutube, } from '@fortawesome/free-brands-svg-icons' import { faClock, @@ -19,6 +19,9 @@ import { } from '@fortawesome/free-regular-svg-icons' import { faArrowsRotate, + faAward, + faBug, + faCertificate, faCity, faCode, faCodeFork, @@ -26,62 +29,59 @@ import { faFlag, faFlask, faGlobe, + faMedal, faMoon, + faPeopleGroup, + faRibbon, faRightToBracket, + faStar as faSolidStar, + faSun, faWandMagicSparkles, faX, - faPeopleGroup, - faSun, - faAward, - faMedal, - faRibbon, - faCertificate, - faBug, - faStar as fasStar, } from '@fortawesome/free-solid-svg-icons' library.add( faArrowsRotate, - faCodeFork, - faStar, - faMedal, faAward, - faRibbon, - faCertificate, faBug, - faUser, + 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: fasStar, - certificate: faCertificate, - bugSlash: faBug, + star: faSolidStar, } as const export const ICONS = {