Skip to content

Commit 59b165e

Browse files
committed
update member with contribution heatmap component
1 parent 7e6e1b5 commit 59b165e

File tree

3 files changed

+80
-87
lines changed

3 files changed

+80
-87
lines changed

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

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { screen, waitFor } from '@testing-library/react'
55
import { render } from 'wrappers/testUtil'
66
import '@testing-library/jest-dom'
77
import UserDetailsPage from 'app/members/[memberKey]/page'
8-
import { drawContributions, fetchHeatmapData } from 'utils/helpers/githubHeatmap'
8+
import { fetchHeatmapData } from 'utils/helpers/githubHeatmap'
99

1010
// Mock Apollo Client
1111
jest.mock('@apollo/client/react', () => ({
@@ -56,9 +56,17 @@ jest.mock('next/navigation', () => ({
5656
// Mock GitHub heatmap utilities
5757
jest.mock('utils/helpers/githubHeatmap', () => ({
5858
fetchHeatmapData: jest.fn(),
59-
drawContributions: jest.fn(() => {}),
6059
}))
6160

61+
jest.mock('components/ContributionHeatmap', () => {
62+
const MockContributionHeatmap = () => <div data-testid="contribution-heatmap">Heatmap</div>
63+
MockContributionHeatmap.displayName = 'MockContributionHeatmap'
64+
return {
65+
__esModule: true,
66+
default: MockContributionHeatmap,
67+
}
68+
})
69+
6270
jest.mock('@heroui/toast', () => ({
6371
addToast: jest.fn(),
6472
}))
@@ -71,9 +79,12 @@ describe('UserDetailsPage', () => {
7179
error: null,
7280
})
7381
;(fetchHeatmapData as jest.Mock).mockResolvedValue({
74-
contributions: { years: [{ year: '2023' }] },
82+
years: [{ year: '2023' }],
83+
contributions: [
84+
{ date: '2023-01-01', count: 5, intensity: '2' },
85+
{ date: '2023-01-02', count: 3, intensity: '1' },
86+
],
7587
})
76-
;(drawContributions as jest.Mock).mockImplementation(() => {})
7788
})
7889

7990
afterEach(() => {
@@ -272,19 +283,19 @@ describe('UserDetailsPage', () => {
272283
loading: false,
273284
})
274285
;(fetchHeatmapData as jest.Mock).mockResolvedValue({
275-
years: [{ year: '2023' }], // Provide years data to satisfy condition in component
286+
years: [{ year: '2023' }],
287+
contributions: [
288+
{ date: '2023-01-01', count: 5, intensity: '2' },
289+
{ date: '2023-01-02', count: 3, intensity: '1' },
290+
],
276291
})
277292

278293
render(<UserDetailsPage />)
279294

280295
// Wait for useEffect to process the fetchHeatmapData result
281296
await waitFor(() => {
282-
const heatmapContainer = screen
283-
.getByAltText('Heatmap Background')
284-
.closest(String.raw`div.hidden.lg\:block`)
285-
expect(heatmapContainer).toBeInTheDocument()
286-
expect(heatmapContainer).toHaveClass('hidden')
287-
expect(heatmapContainer).toHaveClass('lg:block')
297+
const heatmap = screen.getByTestId('contribution-heatmap')
298+
expect(heatmap).toBeInTheDocument()
288299
})
289300
})
290301

@@ -298,8 +309,8 @@ describe('UserDetailsPage', () => {
298309
render(<UserDetailsPage />)
299310

300311
await waitFor(() => {
301-
const heatmapTitle = screen.queryByText('Contribution Heatmap')
302-
expect(heatmapTitle).not.toBeInTheDocument()
312+
const heatmap = screen.queryByTestId('contribution-heatmap')
313+
expect(heatmap).not.toBeInTheDocument()
303314
})
304315
})
305316

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

Lines changed: 39 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,26 @@ import { useQuery } from '@apollo/client/react'
33
import Image from 'next/image'
44
import Link from 'next/link'
55
import { useParams } from 'next/navigation'
6-
import { useTheme } from 'next-themes'
7-
import React, { useState, useEffect, useRef } from 'react'
6+
import React, { useState, useEffect } from 'react'
87
import { FaCodeMerge, FaFolderOpen, FaPersonWalkingArrowRight, FaUserPlus } from 'react-icons/fa6'
98
import { handleAppError, ErrorDisplay } from 'app/global-error'
109

1110
import { GetUserDataDocument } from 'types/__generated__/userQueries.generated'
1211
import { Badge } from 'types/badge'
1312
import { formatDate } from 'utils/dateFormatter'
14-
import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/githubHeatmap'
13+
import { fetchHeatmapData } from 'utils/helpers/githubHeatmap'
1514
import Badges from 'components/Badges'
1615
import DetailsCard from 'components/CardDetailsPage'
16+
import ContributionHeatmap from 'components/ContributionHeatmap'
1717
import MemberDetailsPageSkeleton from 'components/skeletons/MemberDetailsPageSkeleton'
1818

1919
const UserDetailsPage: React.FC = () => {
2020
const { memberKey } = useParams<{ memberKey: string }>()
21-
const [data, setData] = useState<HeatmapData>({} as HeatmapData)
22-
const [username, setUsername] = useState('')
21+
const [contributionData, setContributionData] = useState<Record<string, number>>({})
22+
const [dateRange, setDateRange] = useState<{ startDate: string; endDate: string }>({
23+
startDate: '',
24+
endDate: '',
25+
})
2326
const [isPrivateContributor, setIsPrivateContributor] = useState(false)
2427

2528
const {
@@ -50,9 +53,20 @@ const UserDetailsPage: React.FC = () => {
5053
setIsPrivateContributor(true)
5154
return
5255
}
53-
if (result?.contributions) {
54-
setUsername(memberKey)
55-
setData(result as HeatmapData)
56+
if (result?.contributions && Array.isArray(result.contributions)) {
57+
const transformedData: Record<string, number> = {}
58+
result.contributions.forEach((contribution) => {
59+
transformedData[contribution.date] = contribution.count
60+
})
61+
setContributionData(transformedData)
62+
63+
if (result.contributions.length > 0) {
64+
const dates = result.contributions.map((c) => c.date).sort((a, b) => a.localeCompare(b))
65+
setDateRange({
66+
startDate: dates[0],
67+
endDate: dates.at(-1) ?? '',
68+
})
69+
}
5670
}
5771
}
5872
fetchData()
@@ -128,61 +142,6 @@ const UserDetailsPage: React.FC = () => {
128142
{ icon: FaCodeMerge, value: user?.contributionsCount || 0, unit: 'Contribution' },
129143
]
130144

131-
const Heatmap = () => {
132-
const canvasRef = useRef<HTMLCanvasElement | null>(null)
133-
const [imgSrc, setImgSrc] = useState('')
134-
const { resolvedTheme } = useTheme()
135-
const isDarkMode = (resolvedTheme ?? 'light') === 'dark'
136-
137-
useEffect(() => {
138-
if (canvasRef.current && data?.years?.length) {
139-
drawContributions(canvasRef.current, {
140-
data,
141-
username,
142-
themeName: isDarkMode ? 'dark' : 'light',
143-
})
144-
const imageURL = canvasRef.current.toDataURL()
145-
setImgSrc(imageURL)
146-
} else {
147-
setImgSrc('')
148-
}
149-
}, [isDarkMode])
150-
151-
return (
152-
<div className="overflow-hidden rounded-lg bg-white dark:bg-gray-800">
153-
<div className="relative">
154-
<canvas ref={canvasRef} style={{ display: 'none' }} aria-hidden="true"></canvas>
155-
{imgSrc ? (
156-
<div className="h-32">
157-
<Image
158-
width={100}
159-
height={100}
160-
src={imgSrc}
161-
className="h-full w-full object-cover object-[54%_60%]"
162-
alt="Contribution Heatmap"
163-
/>
164-
</div>
165-
) : (
166-
<div className="relative h-32 items-center justify-center">
167-
<Image
168-
height={100}
169-
width={100}
170-
src={
171-
isDarkMode
172-
? '/img/heatmap-background-dark.png'
173-
: '/img/heatmap-background-light.png'
174-
}
175-
className="heatmap-background-loader h-full w-full border-none object-cover object-[54%_60%]"
176-
alt="Heatmap Background"
177-
/>
178-
<div className="heatmap-loader"></div>
179-
</div>
180-
)}
181-
</div>
182-
</div>
183-
)
184-
}
185-
186145
const UserSummary = () => (
187146
<div className="mt-4 flex flex-col items-center lg:flex-row">
188147
<Image
@@ -192,7 +151,7 @@ const UserDetailsPage: React.FC = () => {
192151
src={user?.avatarUrl || '/placeholder.svg'}
193152
alt={user?.name || user?.login || 'User Avatar'}
194153
/>
195-
<div className="w-full text-center lg:text-left">
154+
<div className="w-full overflow-x-auto text-center lg:text-left">
196155
<div className="pl-0 lg:pl-4">
197156
<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">
198157
<Link
@@ -217,11 +176,22 @@ const UserDetailsPage: React.FC = () => {
217176
</div>
218177
<p className="text-gray-600 dark:text-gray-400">{formattedBio}</p>
219178
</div>
220-
{!isPrivateContributor && (
221-
<div className="hidden w-full lg:block">
222-
<Heatmap />
223-
</div>
224-
)}
179+
{!isPrivateContributor &&
180+
contributionData &&
181+
Object.keys(contributionData).length > 0 &&
182+
dateRange.startDate &&
183+
dateRange.endDate && (
184+
<div className="w-full lg:block">
185+
<div className="overflow-x-auto rounded-lg bg-white dark:bg-gray-800">
186+
<ContributionHeatmap
187+
contributionData={contributionData}
188+
startDate={dateRange.startDate}
189+
endDate={dateRange.endDate}
190+
variant="medium"
191+
/>
192+
</div>
193+
</div>
194+
)}
225195
</div>
226196
</div>
227197
)

frontend/src/components/ContributionHeatmap.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ interface ContributionHeatmapProps {
249249
endDate: string
250250
title?: string
251251
unit?: string
252-
variant?: 'default' | 'compact'
252+
variant?: 'default' | 'medium' | 'compact'
253253
}
254254

255255
const ContributionHeatmap: React.FC<ContributionHeatmapProps> = ({
@@ -262,7 +262,6 @@ const ContributionHeatmap: React.FC<ContributionHeatmapProps> = ({
262262
}) => {
263263
const { theme } = useTheme()
264264
const isDarkMode = theme === 'dark'
265-
const isCompact = variant === 'compact'
266265

267266
const { heatmapSeries } = useMemo(
268267
() => generateHeatmapSeries(startDate, endDate, contributionData),
@@ -274,21 +273,34 @@ const ContributionHeatmap: React.FC<ContributionHeatmapProps> = ({
274273
const calculateChartWidth = useMemo(() => {
275274
const weeksCount = heatmapSeries[0]?.data?.length || 0
276275

277-
if (isCompact) {
276+
if (variant === 'compact') {
278277
const pixelPerWeek = 13.4
279278
const padding = 40
280279
const calculatedWidth = weeksCount * pixelPerWeek + padding
281280
return Math.max(400, calculatedWidth)
282281
}
283282

283+
if (variant === 'medium') {
284+
const pixelPerWeek = 15.5
285+
const padding = 40
286+
const calculatedWidth = weeksCount * pixelPerWeek + padding
287+
return Math.max(500, calculatedWidth)
288+
}
289+
284290
const pixelPerWeek = 19.5
285291
const padding = 50
286292
const calculatedWidth = weeksCount * pixelPerWeek + padding
287293
return Math.max(600, calculatedWidth)
288-
}, [heatmapSeries, isCompact])
294+
}, [heatmapSeries, variant])
289295

290296
const chartWidth = calculateChartWidth
291297

298+
const getChartHeight = () => {
299+
if (variant === 'compact') return 150
300+
if (variant === 'medium') return 172
301+
return 195
302+
}
303+
292304
return (
293305
<div className="w-full">
294306
{title && (
@@ -316,7 +328,7 @@ const ContributionHeatmap: React.FC<ContributionHeatmapProps> = ({
316328

317329
<div className="inline-block">
318330
<Chart
319-
height={isCompact ? 150 : 195}
331+
height={getChartHeight()}
320332
options={options}
321333
series={heatmapSeries}
322334
type="heatmap"

0 commit comments

Comments
 (0)