Skip to content

Commit 848ae72

Browse files
committed
Fix issues with badges
1 parent 6e1bbe7 commit 848ae72

File tree

12 files changed

+69
-54
lines changed

12 files changed

+69
-54
lines changed

backend/apps/core/utils/index.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def get_params_for_index(index_name: str) -> dict:
196196
case "users":
197197
params["attributesToRetrieve"] = [
198198
"idx_avatar_url",
199+
"idx_badge_count",
199200
"idx_bio",
200201
"idx_company",
201202
"idx_created_at",

backend/apps/github/index/registry/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class UserIndex(IndexBase):
1414

1515
fields = (
1616
"idx_avatar_url",
17+
"idx_badge_count",
1718
"idx_bio",
1819
"idx_company",
1920
"idx_contributions",

backend/apps/github/index/search/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_users(
3232
"attributesToRetrieve": attributes
3333
or [
3434
"idx_avatar_url",
35+
"idx_badge_count",
3536
"idx_bio",
3637
"idx_company",
3738
"idx_contributions",

backend/apps/github/models/mixins/user.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,8 @@ def idx_updated_at(self) -> float:
182182
def idx_url(self) -> str:
183183
"""Return GitHub profile URL for indexing."""
184184
return self.url
185+
186+
@property
187+
def idx_badge_count(self) -> int:
188+
"""Return badge count for indexing."""
189+
return self.user_badges.filter(is_active=True).count()

frontend/__tests__/unit/components/Badges.test.tsx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,10 @@ import { render, screen } from '@testing-library/react'
22
import React from 'react'
33
import Badges from 'components/Badges'
44

5-
jest.mock('@fortawesome/fontawesome-svg-core', () => {
6-
const { BADGE_CLASS_MAP } = jest.requireActual('utils/data')
7-
const registered = new Set(
8-
Object.values(BADGE_CLASS_MAP).map((s: string) => s.split(' ').pop()?.replace('fa-', ''))
9-
)
10-
11-
return {
12-
findIconDefinition: jest.fn(({ iconName }: { iconName: string }) => {
13-
return registered.has(iconName) ? { iconName } : null
14-
}),
15-
}
16-
})
17-
185
jest.mock('wrappers/FontAwesomeIconWrapper', () => {
196
const RealWrapper = jest.requireActual('wrappers/FontAwesomeIconWrapper').default
207

21-
const getName = (icon: any) => {
8+
const getName = (icon) => {
229
if (!icon) return 'medal'
2310
if (typeof icon === 'string') {
2411
const m = icon.match(/fa-([a-z0-9-]+)$/i)
@@ -31,7 +18,7 @@ jest.mock('wrappers/FontAwesomeIconWrapper', () => {
3118
return 'medal'
3219
}
3320

34-
return function MockFontAwesomeIconWrapper(props: any) {
21+
return function MockFontAwesomeIconWrapper(props) {
3522
const name = getName(props.icon)
3623
return (
3724
<div data-testid={`icon-${name}`}>
@@ -110,17 +97,25 @@ describe('Badges Component', () => {
11097
{ cssClass: 'ribbon', expectedIcon: 'ribbon' },
11198
{ cssClass: 'star', expectedIcon: 'star' },
11299
{ cssClass: 'certificate', expectedIcon: 'certificate' },
113-
{ cssClass: 'bug-slash', expectedIcon: 'bug' },
100+
{ cssClass: 'bug_slash', expectedIcon: 'bug' }, // Backend snake_case input
114101
]
115102

116103
backendIcons.forEach(({ cssClass, expectedIcon }) => {
117-
it(`renders ${cssClass} icon correctly`, () => {
104+
it(`renders ${cssClass} icon correctly (transforms snake_case to camelCase)`, () => {
118105
render(<Badges name={`${cssClass} Badge`} cssClass={cssClass} />)
119106

120107
const icon = screen.getByTestId('badge-icon')
121108
expect(icon).toBeInTheDocument()
122109
expect(icon).toHaveAttribute('data-icon', expectedIcon)
123110
})
124111
})
112+
113+
it('handles camelCase input directly', () => {
114+
render(<Badges name="Bug Slash Badge" cssClass="bugSlash" />)
115+
116+
const icon = screen.getByTestId('badge-icon')
117+
expect(icon).toBeInTheDocument()
118+
expect(icon).toHaveAttribute('data-icon', 'bug')
119+
})
125120
})
126121
})

frontend/__tests__/unit/pages/UserDetails.test.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ describe('UserDetailsPage', () => {
538538

539539
describe('Badge Display Tests', () => {
540540
test('renders badges section when user has badges', async () => {
541-
;(useQuery as jest.Mock).mockReturnValue({
541+
;(useQuery as unknown as jest.Mock).mockReturnValue({
542542
data: mockUserDetailsData,
543543
loading: false,
544544
error: null,
@@ -552,7 +552,7 @@ describe('UserDetailsPage', () => {
552552
})
553553

554554
test('renders badges with correct props', async () => {
555-
;(useQuery as jest.Mock).mockReturnValue({
555+
;(useQuery as unknown as jest.Mock).mockReturnValue({
556556
data: mockUserDetailsData,
557557
loading: false,
558558
error: null,
@@ -580,7 +580,7 @@ describe('UserDetailsPage', () => {
580580
},
581581
}
582582

583-
;(useQuery as jest.Mock).mockReturnValue({
583+
;(useQuery as unknown as jest.Mock).mockReturnValue({
584584
data: dataWithoutBadges,
585585
loading: false,
586586
error: null,
@@ -602,7 +602,7 @@ describe('UserDetailsPage', () => {
602602
},
603603
}
604604

605-
;(useQuery as jest.Mock).mockReturnValue({
605+
;(useQuery as unknown as jest.Mock).mockReturnValue({
606606
data: dataWithoutBadges,
607607
loading: false,
608608
error: null,
@@ -631,7 +631,7 @@ describe('UserDetailsPage', () => {
631631
},
632632
}
633633

634-
;(useQuery as jest.Mock).mockReturnValue({
634+
;(useQuery as unknown as jest.Mock).mockReturnValue({
635635
data: dataWithIncompleteBadges,
636636
loading: false,
637637
error: null,
@@ -661,7 +661,7 @@ describe('UserDetailsPage', () => {
661661
},
662662
}
663663

664-
;(useQuery as jest.Mock).mockReturnValue({
664+
;(useQuery as unknown as jest.Mock).mockReturnValue({
665665
data: dataWithEmptyCssClass,
666666
loading: false,
667667
error: null,
@@ -690,7 +690,7 @@ describe('UserDetailsPage', () => {
690690
],
691691
},
692692
}
693-
;(useQuery as jest.Mock).mockReturnValue({
693+
;(useQuery as unknown as jest.Mock).mockReturnValue({
694694
data: dataWithSpecialBadges,
695695
loading: false,
696696
error: null,
@@ -719,7 +719,7 @@ describe('UserDetailsPage', () => {
719719
},
720720
}
721721

722-
;(useQuery as jest.Mock).mockReturnValue({
722+
;(useQuery as unknown as jest.Mock).mockReturnValue({
723723
data: dataWithLongNameBadge,
724724
loading: false,
725725
error: null,

frontend/src/app/members/[memberKey]/page.tsx

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -197,25 +197,30 @@ const UserDetailsPage: React.FC = () => {
197197
alt={user?.name || user?.login || 'User Avatar'}
198198
/>
199199
<div className="w-full text-center lg:text-left">
200-
<div className="flex gap-5 text-center text-sm text-gray-500 lg:text-left dark:text-gray-400">
201-
<Link href={user?.url || '#'} className="text-xl font-bold text-blue-400 hover:underline">
202-
@{user?.login}
203-
</Link>
204-
{user?.badges && user.badges.length > 0 && (
205-
<div className="mt-2 flex flex-wrap gap-2">
206-
{user.badges.slice().map((badge: Badge) => (
207-
<React.Fragment key={badge.id}>
208-
<Badges
209-
name={badge.name}
210-
cssClass={badge.cssClass || 'fa-medal'}
211-
showTooltip={true}
212-
/>
213-
</React.Fragment>
214-
))}
215-
</div>
216-
)}
200+
<div className="pl-0 lg:pl-4">
201+
<div className="flex items-center justify-center gap-3 text-center text-sm text-gray-500 lg:justify-start lg:text-left dark:text-gray-400">
202+
<Link
203+
href={user?.url || '#'}
204+
className="text-xl font-bold text-blue-400 hover:underline"
205+
>
206+
@{user?.login}
207+
</Link>
208+
{user?.badges && user.badges.length > 0 && (
209+
<div className="flex flex-wrap gap-2">
210+
{user.badges.slice().map((badge: Badge) => (
211+
<React.Fragment key={badge.id}>
212+
<Badges
213+
name={badge.name}
214+
cssClass={badge.cssClass || 'fa-medal'}
215+
showTooltip={true}
216+
/>
217+
</React.Fragment>
218+
))}
219+
</div>
220+
)}
221+
</div>
222+
<p className="text-gray-600 dark:text-gray-400">{formattedBio}</p>
217223
</div>
218-
<p className="text-gray-600 dark:text-gray-400">{formattedBio}</p>
219224
{!isPrivateContributor && (
220225
<div className="hidden w-full lg:block">
221226
<Heatmap />

frontend/src/app/members/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const UsersPage = () => {
2222
})
2323

2424
const router = useRouter()
25-
2625
const handleButtonClick = (user: User) => {
2726
router.push(`/members/${user.key}`)
2827
}
@@ -35,7 +34,6 @@ const UsersPage = () => {
3534
}
3635

3736
const badgeCount = user.badgeCount || 0
38-
3937
return (
4038
<UserCard
4139
avatar={user.avatarUrl}

frontend/src/components/Badges.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ type BadgeProps = {
1010

1111
const DEFAULT_ICON = BADGE_CLASS_MAP['medal']
1212

13+
const normalizeCssClass = (cssClass: string) => {
14+
// Convert backend snake_case format to frontend camelCase format
15+
return cssClass.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
16+
}
17+
1318
const resolveIcon = (cssClass: string) => {
14-
return BADGE_CLASS_MAP[cssClass] ?? DEFAULT_ICON
19+
const normalizedClass = normalizeCssClass(cssClass)
20+
return BADGE_CLASS_MAP[normalizedClass] ?? DEFAULT_ICON
1521
}
1622

1723
const Badges = ({ name, cssClass, showTooltip = true }: BadgeProps) => {

frontend/src/server/queries/userQueries.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const GET_LEADER_DATA = gql`
1414
name
1515
description
1616
cssClass
17+
weight
1718
}
1819
badgeCount
1920
}
@@ -92,6 +93,7 @@ export const GET_USER_DATA = gql`
9293
description
9394
id
9495
name
96+
weight
9597
}
9698
badgeCount
9799
publicRepositoriesCount

0 commit comments

Comments
 (0)