@@ -13,6 +13,7 @@ export default function CommitClock({ data }: { data: CommitClockData[] }) {
1313
1414 // Find the peak day index
1515 const peakIndex = data . reduce ( ( peak , d , i ) => ( d . commits > data [ peak ] . commits ? i : peak ) , 0 ) ;
16+ const hasData = data . length > 0 && data . some ( ( d ) => d . commits > 0 ) ;
1617
1718 return (
1819 < motion . div
@@ -30,16 +31,17 @@ export default function CommitClock({ data }: { data: CommitClockData[] }) {
3031 </ div >
3132
3233 < div className = "relative w-[280px] h-[280px] flex items-center justify-center mt-4" >
33- < svg width = "280" height = "280" className = "overflow-visible rotate-[-90deg]" >
34- < defs >
35- < filter id = "spoke-glow" x = "-50%" y = "-50%" width = "200%" height = "200%" >
36- < feGaussianBlur stdDeviation = "3" result = "blur" />
37- < feComposite in = "SourceGraphic" in2 = "blur" operator = "over" />
38- </ filter >
39- </ defs >
40-
41- < style >
42- { `
34+ { hasData ? (
35+ < svg width = "280" height = "280" className = "overflow-visible rotate-[-90deg]" >
36+ < defs >
37+ < filter id = "spoke-glow" x = "-50%" y = "-50%" width = "200%" height = "200%" >
38+ < feGaussianBlur stdDeviation = "3" result = "blur" />
39+ < feComposite in = "SourceGraphic" in2 = "blur" operator = "over" />
40+ </ filter >
41+ </ defs >
42+
43+ < style >
44+ { `
4345 :root {
4446 --peak-spoke: #111111;
4547 --peak-dot: rgba(17,17,17,0.7);
@@ -52,143 +54,148 @@ export default function CommitClock({ data }: { data: CommitClockData[] }) {
5254 --peak-label: #ffffff;
5355 }
5456 ` }
55- </ style >
56-
57- { /* Base ring */ }
58- < circle
59- cx = { cx }
60- cy = { cy }
61- r = { radius }
62- fill = "none"
63- stroke = "rgba(120,120,120,0.15)"
64- strokeWidth = "18"
65- />
66-
67- { /* Outer boundary ring */ }
68- < circle
69- cx = { cx }
70- cy = { cy }
71- r = { radius + maxSpokeLength + 8 }
72- fill = "none"
73- stroke = "rgba(120,120,120,0.22)"
74- strokeWidth = "1"
75- />
76-
77- { /* Subtle background dial (35 ticks = 7 days * 5 sub-intervals) */ }
78- { Array . from ( { length : 35 } ) . map ( ( _ , i ) => {
79- const angle = ( i * 360 ) / 35 ;
80- const rad = ( angle * Math . PI ) / 180 ;
81- const isMain = i % 5 === 0 ;
82-
83- // Main spokes get a full-length faint track, interval ticks are short
84- const tickLength = isMain ? maxSpokeLength : 4 ;
85- const innerOffset = isMain ? 0 : 4 ;
86-
87- return (
88- < line
89- key = { `bg-tick-${ i } ` }
90- x1 = { r4 ( cx + ( radius + innerOffset ) * Math . cos ( rad ) ) }
91- y1 = { r4 ( cy + ( radius + innerOffset ) * Math . sin ( rad ) ) }
92- x2 = { r4 ( cx + ( radius + tickLength ) * Math . cos ( rad ) ) }
93- y2 = { r4 ( cy + ( radius + tickLength ) * Math . sin ( rad ) ) }
94- stroke = { isMain ? 'rgba(120,120,120,0.28)' : 'rgba(120,120,120,0.14)' }
95- strokeWidth = { isMain ? '4' : '2' }
96- strokeLinecap = "round"
97- />
98- ) ;
99- } ) }
100-
101- { /* Spokes — one per day of week */ }
102- { data . map ( ( d , i ) => {
103- const angle = ( i * 360 ) / 7 ;
104- const length = Math . max ( ( d . commits / maxCommits ) * maxSpokeLength , 4 ) ;
105- const isHigh = d . commits > maxCommits * 0.7 ;
106- const isPeak = i === peakIndex && d . commits > 0 ;
107- const rad = ( angle * Math . PI ) / 180 ;
108-
109- // Scale stroke width: 3px base, up to 6px for peak
110- const strokeW = isPeak ? 6 : isHigh ? 5 : 4 ;
111-
112- const x1 = r4 ( cx + radius * Math . cos ( rad ) ) ;
113- const y1 = r4 ( cy + radius * Math . sin ( rad ) ) ;
114- const x2 = r4 ( cx + ( radius + length ) * Math . cos ( rad ) ) ;
115- const y2 = r4 ( cy + ( radius + length ) * Math . sin ( rad ) ) ;
116- const labelX = r4 ( cx + ( radius + maxSpokeLength + 14 ) * Math . cos ( rad ) ) ;
117- const labelY = r4 ( cy + ( radius + maxSpokeLength + 14 ) * Math . sin ( rad ) ) ;
118-
119- const spokeColor = isPeak
120- ? 'var(--peak-spoke)'
121- : isHigh
122- ? 'rgba(160,160,160,0.85)'
123- : 'rgba(110,110,110,0.55)' ;
124-
125- const dotColor = isPeak
126- ? 'var(--peak-dot)'
127- : isHigh
128- ? 'rgba(180,180,180,0.75)'
129- : 'rgba(120,120,120,0.55)' ;
130-
131- const labelColor = isPeak
132- ? 'var(--peak-label)'
133- : isHigh
134- ? 'rgba(180,180,180,0.75)'
135- : 'rgba(120,120,120,0.6)' ;
136- return (
137- < motion . g
138- key = { d . day }
139- initial = { { opacity : 0 } }
140- animate = { { opacity : 1 } }
141- transition = { { delay : i * 0.08 , duration : 0.3 } }
142- >
57+ </ style >
58+
59+ { /* Base ring */ }
60+ < circle
61+ cx = { cx }
62+ cy = { cy }
63+ r = { radius }
64+ fill = "none"
65+ stroke = "rgba(120,120,120,0.15)"
66+ strokeWidth = "18"
67+ />
68+
69+ { /* Outer boundary ring */ }
70+ < circle
71+ cx = { cx }
72+ cy = { cy }
73+ r = { radius + maxSpokeLength + 8 }
74+ fill = "none"
75+ stroke = "rgba(120,120,120,0.22)"
76+ strokeWidth = "1"
77+ />
78+
79+ { /* Subtle background dial (35 ticks = 7 days * 5 sub-intervals) */ }
80+ { Array . from ( { length : 35 } ) . map ( ( _ , i ) => {
81+ const angle = ( i * 360 ) / 35 ;
82+ const rad = ( angle * Math . PI ) / 180 ;
83+ const isMain = i % 5 === 0 ;
84+
85+ // Main spokes get a full-length faint track, interval ticks are short
86+ const tickLength = isMain ? maxSpokeLength : 4 ;
87+ const innerOffset = isMain ? 0 : 4 ;
88+
89+ return (
14390 < line
144- x1 = { x1 }
145- y1 = { y1 }
146- x2 = { x2 }
147- y2 = { y2 }
148- stroke = { spokeColor }
149- strokeWidth = { strokeW }
91+ key = { `bg-tick-${ i } ` }
92+ x1 = { r4 ( cx + ( radius + innerOffset ) * Math . cos ( rad ) ) }
93+ y1 = { r4 ( cy + ( radius + innerOffset ) * Math . sin ( rad ) ) }
94+ x2 = { r4 ( cx + ( radius + tickLength ) * Math . cos ( rad ) ) }
95+ y2 = { r4 ( cy + ( radius + tickLength ) * Math . sin ( rad ) ) }
96+ stroke = { isMain ? 'rgba(120,120,120,0.28)' : 'rgba(120,120,120,0.14)' }
97+ strokeWidth = { isMain ? '4' : '2' }
15098 strokeLinecap = "round"
151- filter = { isPeak ? 'url(#spoke-glow)' : undefined }
15299 />
153-
154- { /* Dot at the end of each spoke */ }
155- { d . commits > 0 && (
156- < circle
157- cx = { x2 }
158- cy = { y2 }
159- r = { isPeak ? 2.2 : 2 }
160- fill = { dotColor }
100+ ) ;
101+ } ) }
102+
103+ { /* Spokes — one per day of week */ }
104+ { data . map ( ( d , i ) => {
105+ const angle = ( i * 360 ) / 7 ;
106+ const length = Math . max ( ( d . commits / maxCommits ) * maxSpokeLength , 4 ) ;
107+ const isHigh = d . commits > maxCommits * 0.7 ;
108+ const isPeak = i === peakIndex && d . commits > 0 ;
109+ const rad = ( angle * Math . PI ) / 180 ;
110+
111+ // Scale stroke width: 3px base, up to 6px for peak
112+ const strokeW = isPeak ? 6 : isHigh ? 5 : 4 ;
113+
114+ const x1 = r4 ( cx + radius * Math . cos ( rad ) ) ;
115+ const y1 = r4 ( cy + radius * Math . sin ( rad ) ) ;
116+ const x2 = r4 ( cx + ( radius + length ) * Math . cos ( rad ) ) ;
117+ const y2 = r4 ( cy + ( radius + length ) * Math . sin ( rad ) ) ;
118+ const labelX = r4 ( cx + ( radius + maxSpokeLength + 14 ) * Math . cos ( rad ) ) ;
119+ const labelY = r4 ( cy + ( radius + maxSpokeLength + 14 ) * Math . sin ( rad ) ) ;
120+
121+ const spokeColor = isPeak
122+ ? 'var(--peak-spoke)'
123+ : isHigh
124+ ? 'rgba(160,160,160,0.85)'
125+ : 'rgba(110,110,110,0.55)' ;
126+
127+ const dotColor = isPeak
128+ ? 'var(--peak-dot)'
129+ : isHigh
130+ ? 'rgba(180,180,180,0.75)'
131+ : 'rgba(120,120,120,0.55)' ;
132+
133+ const labelColor = isPeak
134+ ? 'var(--peak-label)'
135+ : isHigh
136+ ? 'rgba(180,180,180,0.75)'
137+ : 'rgba(120,120,120,0.6)' ;
138+ return (
139+ < motion . g
140+ key = { d . day }
141+ initial = { { opacity : 0 } }
142+ animate = { { opacity : 1 } }
143+ transition = { { delay : i * 0.08 , duration : 0.3 } }
144+ >
145+ < line
146+ x1 = { x1 }
147+ y1 = { y1 }
148+ x2 = { x2 }
149+ y2 = { y2 }
150+ stroke = { spokeColor }
151+ strokeWidth = { strokeW }
152+ strokeLinecap = "round"
161153 filter = { isPeak ? 'url(#spoke-glow)' : undefined }
162154 />
163- ) }
164-
165- { /* Day and Commit Count Label (stacked vertically via tspan to avoid side overlaps) */ }
166- < text
167- x = { labelX }
168- y = { labelY }
169- fill = { isPeak ? 'var(--peak-label)' : 'rgba(120,120,120,0.55)' }
170- fontSize = "9"
171- fontWeight = { isPeak ? '700' : '400' }
172- textAnchor = "middle"
173- transform = { `rotate(90, ${ labelX } , ${ labelY } )` }
174- >
175- < tspan x = { labelX } dy = "-2" >
176- { d . day }
177- </ tspan >
178- < tspan
155+
156+ { /* Dot at the end of each spoke */ }
157+ { d . commits > 0 && (
158+ < circle
159+ cx = { x2 }
160+ cy = { y2 }
161+ r = { isPeak ? 2.2 : 2 }
162+ fill = { dotColor }
163+ filter = { isPeak ? 'url(#spoke-glow)' : undefined }
164+ />
165+ ) }
166+
167+ { /* Day and Commit Count Label (stacked vertically via tspan to avoid side overlaps) */ }
168+ < text
179169 x = { labelX }
180- dy = "10"
181- fill = { isPeak ? 'var(--peak-label)' : labelColor }
182- fontSize = "7"
183- fontWeight = "700"
170+ y = { labelY }
171+ fill = { isPeak ? 'var(--peak-label)' : 'rgba(120,120,120,0.55)' }
172+ fontSize = "9"
173+ fontWeight = { isPeak ? '700' : '400' }
174+ textAnchor = "middle"
175+ transform = { `rotate(90, ${ labelX } , ${ labelY } )` }
184176 >
185- { d . commits }
186- </ tspan >
187- </ text >
188- </ motion . g >
189- ) ;
190- } ) }
191- </ svg >
177+ < tspan x = { labelX } dy = "-2" >
178+ { d . day }
179+ </ tspan >
180+ < tspan
181+ x = { labelX }
182+ dy = "10"
183+ fill = { isPeak ? 'var(--peak-label)' : labelColor }
184+ fontSize = "7"
185+ fontWeight = "700"
186+ >
187+ { d . commits }
188+ </ tspan >
189+ </ text >
190+ </ motion . g >
191+ ) ;
192+ } ) }
193+ </ svg >
194+ ) : (
195+ < div className = "h-[280px] flex items-center justify-center w-full rounded-lg border border-dashed border-black/10 dark:border-[rgba(255,255,255,0.08)] text-sm text-[#A1A1AA]" >
196+ No recent activity to display
197+ </ div >
198+ ) }
192199
193200 { /* Center label */ }
194201 < div className = "absolute inset-0 flex flex-col items-center justify-center pointer-events-none" >
0 commit comments