1
+ <!DOCTYPE html>
2
+ < html lang ="en ">
3
+ < head >
4
+ < meta charset ="UTF-8 ">
5
+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
6
+ < title > LeetCode User Stats</ title >
7
+ < script src ="https://cdn.tailwindcss.com "> </ script >
8
+ < script src ="https://cdn.jsdelivr.net/npm/chart.js "> </ script >
9
+ < script src ="https://cdn.jsdelivr.net/npm/apexcharts "> </ script >
10
+ < script >
11
+ tailwind . config = {
12
+ theme : {
13
+ extend : {
14
+ colors : {
15
+ primary : '#58a6ff' ,
16
+ } ,
17
+ backgroundImage : {
18
+ 'gradient' : 'linear-gradient(45deg, hsl(240deg 100% 20%) 0%, hsl(281deg 100% 21%) 8%, hsl(304deg 100% 23%) 17%, hsl(319deg 100% 30%) 25%, hsl(329deg 100% 36%) 33%, hsl(336deg 100% 41%) 42%, hsl(346deg 83% 51%) 50%, hsl(3deg 95% 61%) 58%, hsl(17deg 100% 59%) 67%, hsl(30deg 100% 55%) 75%, hsl(40deg 100% 50%) 83%, hsl(48deg 100% 50%) 92%, hsl(55deg 100% 50%) 100%)' ,
19
+ } ,
20
+ }
21
+ }
22
+ }
23
+ </ script >
24
+ < style >
25
+ .glassmorphism {
26
+ background : rgba (255 , 255 , 255 , 0.05 );
27
+ backdrop-filter : blur (10px );
28
+ border-radius : 10px ;
29
+ border : 1px solid rgba (255 , 255 , 255 , 0.1 );
30
+ }
31
+ .glass-2 {
32
+ background : rgba (255 , 255 , 255 , 0.05 );
33
+ backdrop-filter : blur (10px );
34
+ border : 1px solid rgba (255 , 255 , 255 , 0.1 );
35
+ }
36
+ .spinner {
37
+ border : 4px solid rgba (255 , 255 , 255 , 0.3 );
38
+ border-radius : 50% ;
39
+ border-top : 4px solid # ffffff ;
40
+ width : 40px ;
41
+ height : 40px ;
42
+ animation : spin 1s linear infinite;
43
+ }
44
+
45
+ @keyframes spin {
46
+ 0% { transform : rotate (0deg ); }
47
+ 100% { transform : rotate (360deg ); }
48
+ }
49
+
50
+ .contribution-cell {
51
+ width : 10px ;
52
+ height : 10px ;
53
+ margin : 2px ;
54
+ border-radius : 2px ;
55
+ transition : all 0.2s ease;
56
+ }
57
+
58
+ .contribution-cell : hover {
59
+ transform : scale (1.2 );
60
+ }
61
+
62
+ .level-0 { background-color : # 161b22 ; }
63
+ .level-1 { background-color : # 0e4429 ; }
64
+ .level-2 { background-color : # 006d32 ; }
65
+ .level-3 { background-color : # 26a641 ; }
66
+ .level-4 { background-color : # 39d353 ; }
67
+ </ style >
68
+ </ head >
69
+ < body class ="bg-gradient text-white min-h-screen ">
70
+ < div class ="container mx-auto px-4 py-8 ">
71
+ < h1 class ="text-2xl flex items-center justify-center gap-2 font-bold text-white mb-8 text-center "> < img class ="h-10 " src ="https://upload.wikimedia.org/wikipedia/commons/0/0a/LeetCode_Logo_black_with_text.svg "/> api</ h1 >
72
+ < div class ="mb-6 flex justify-center ">
73
+ < input type ="text " id ="username " placeholder ="Username " class ="bg-white bg-opacity-20 text-white px-4 py-2 rounded-l-md focus:outline-none focus:ring-0 focus:ring-none placeholder-gray-300 ">
74
+ < button onclick ="fetchUserStats() " class ="glass-2 text-white px-4 py-2 rounded-r-md hover:bg-opacity-90 transition-colors "> Okay</ button >
75
+ </ div >
76
+ < div id ="userStats " class ="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8 "> </ div >
77
+ < div id ="heatmap " class ="mb-8 glassmorphism p-4 ">
78
+
79
+ < div id ="heatmapChart "> </ div >
80
+ </ div >
81
+ < div id ="recentSubmissions " class ="glassmorphism p-4 ">
82
+
83
+ </ div >
84
+ < div id ="loadingSpinner " class ="fixed top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-50 z-50 hidden ">
85
+ < div class ="spinner "> </ div >
86
+ </ div >
87
+
88
+
89
+ </ div >
90
+ < div class ="glassmorphism py-3 "> < a target ="_blank " href ="https://github.com/faisal-shohag/leetcode_api "> < div class ="flex items-center gap-2 font-semibold justify-center ">
91
+ Star at < img class ="h-6 w-6 " src ="https://seeklogo.com/images/G/github-logo-7880D80B8D-seeklogo.com.png "/>
92
+ </ div > </ a > </ div >
93
+
94
+ < script >
95
+ async function fetchUserStats ( ) {
96
+ const username = document . getElementById ( 'username' ) . value ;
97
+ document . getElementById ( 'loadingSpinner' ) . classList . remove ( 'hidden' ) ;
98
+ try {
99
+ const response = await fetch ( `https://leetcode-api-faisalshohag.vercel.app/${ username } ` ) ;
100
+ const data = await response . json ( ) ;
101
+ displayUserStats ( data ) ;
102
+ createHeatmap ( data . submissionCalendar ) ;
103
+ displayRecentSubmissions ( data . recentSubmissions ) ;
104
+ } catch ( error ) {
105
+ console . error ( 'Error fetching user stats:' , error ) ;
106
+ } finally {
107
+ document . getElementById ( 'loadingSpinner' ) . classList . add ( 'hidden' ) ;
108
+ }
109
+ }
110
+
111
+ function displayUserStats ( data ) {
112
+ // Previous displayUserStats code remains the same
113
+ const statsHtml = `
114
+ <div class="glassmorphism p-6 shadow-lg">
115
+ <h3 class="text-xl font-semibold text-white mb-2">Total Solved</h3>
116
+ <div class="flex items-center justify-between">
117
+ <p class="text-3xl font-bold">${ data . totalSolved } /${ data . totalQuestions } </p>
118
+ <canvas id="totalSolvedChart" width="80" height="80"></canvas>
119
+ </div>
120
+ </div>
121
+ <div class="glassmorphism p-6 shadow-lg">
122
+ <h3 class="text-xl font-semibold text-white mb-2">Ranking</h3>
123
+ <p class="text-3xl font-bold">${ data . ranking } </p>
124
+ </div>
125
+ <div class="glassmorphism p-6 shadow-lg">
126
+ <h3 class="text-xl font-semibold text-white mb-2">Easy Solved</h3>
127
+ <div class="flex items-center justify-between">
128
+ <p class="text-3xl font-bold">${ data . easySolved } /${ data . totalEasy } </p>
129
+ <canvas id="easySolvedChart" width="80" height="80"></canvas>
130
+ </div>
131
+ </div>
132
+ <div class="glassmorphism p-6 shadow-lg">
133
+ <h3 class="text-xl font-semibold text-white mb-2">Medium Solved</h3>
134
+ <div class="flex items-center justify-between">
135
+ <p class="text-3xl font-bold">${ data . mediumSolved } /${ data . totalMedium } </p>
136
+ <canvas id="mediumSolvedChart" width="80" height="80"></canvas>
137
+ </div>
138
+ </div>
139
+ <div class="glassmorphism p-6 shadow-lg">
140
+ <h3 class="text-xl font-semibold text-white mb-2">Hard Solved</h3>
141
+ <div class="flex items-center justify-between">
142
+ <p class="text-3xl font-bold">${ data . hardSolved } /${ data . totalHard } </p>
143
+ <canvas id="hardSolvedChart" width="80" height="80"></canvas>
144
+ </div>
145
+ </div>
146
+ <div class="glassmorphism p-6 shadow-lg">
147
+ <h3 class="text-xl font-semibold text-white mb-2">Contribution Points</h3>
148
+ <p class="text-3xl font-bold">${ data . contributionPoint } </p>
149
+ </div>
150
+ ` ;
151
+ document . getElementById ( 'userStats' ) . innerHTML = statsHtml ;
152
+
153
+ createDonutChart ( 'totalSolvedChart' , data . totalSolved , data . totalQuestions , '#3B82F6' ) ;
154
+ createDonutChart ( 'easySolvedChart' , data . easySolved , data . totalEasy , '#10B981' ) ;
155
+ createDonutChart ( 'mediumSolvedChart' , data . mediumSolved , data . totalMedium , '#FBBF24' ) ;
156
+ createDonutChart ( 'hardSolvedChart' , data . hardSolved , data . totalHard , '#EF4444' ) ;
157
+ }
158
+
159
+ function createDonutChart ( elementId , value , total , color ) {
160
+ const ctx = document . getElementById ( elementId ) . getContext ( '2d' ) ;
161
+ new Chart ( ctx , {
162
+ type : 'doughnut' ,
163
+ data : {
164
+ datasets : [ {
165
+ data : [ value , total - value ] ,
166
+ backgroundColor : [ color , 'rgba(255, 255, 255, 0.2)' ] ,
167
+ borderWidth : 0
168
+ } ]
169
+ } ,
170
+ options : {
171
+ cutout : '70%' ,
172
+ responsive : false ,
173
+ maintainAspectRatio : false ,
174
+ plugins : {
175
+ legend : {
176
+ display : false
177
+ } ,
178
+ tooltip : {
179
+ enabled : false
180
+ }
181
+ }
182
+ }
183
+ } ) ;
184
+ }
185
+
186
+ function createHeatmap ( submissionCalendar ) {
187
+ const heatmapContainer = document . querySelector ( "#heatmapChart" ) ;
188
+ heatmapContainer . innerHTML = '<h2 class="text-2xl font-bold text-white mb-4">Submission Heatmap</h2>' ; // Clear existing content
189
+
190
+ // Create container for the entire heatmap
191
+ const container = document . createElement ( 'div' ) ;
192
+ container . className = 'flex flex-col gap-2' ;
193
+
194
+ // Create month labels
195
+ const monthsContainer = document . createElement ( 'div' ) ;
196
+ monthsContainer . className = 'flex text-xs text-gray-400 mb-1' ;
197
+ const months = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
198
+ months . forEach ( month => {
199
+ const monthLabel = document . createElement ( 'div' ) ;
200
+ monthLabel . className = 'flex-1 text-center' ;
201
+ monthLabel . textContent = month ;
202
+ monthsContainer . appendChild ( monthLabel ) ;
203
+ } ) ;
204
+ container . appendChild ( monthsContainer ) ;
205
+
206
+ // Create grid container
207
+ const gridContainer = document . createElement ( 'div' ) ;
208
+ gridContainer . className = 'flex gap-2' ;
209
+
210
+ // Add day labels
211
+ const dayLabels = document . createElement ( 'div' ) ;
212
+ dayLabels . className = 'flex flex-col gap-8 text-xs text-gray-400 pr-2' ;
213
+ [ 'Mon' , 'Wed' , 'Fri' ] . forEach ( day => {
214
+ const dayLabel = document . createElement ( 'div' ) ;
215
+ dayLabel . textContent = day ;
216
+ dayLabels . appendChild ( dayLabel ) ;
217
+ } ) ;
218
+ gridContainer . appendChild ( dayLabels ) ;
219
+
220
+ // Process and create contribution grid
221
+ const { weeks } = processCalendarData ( submissionCalendar ) ;
222
+ const contributionGrid = document . createElement ( 'div' ) ;
223
+ contributionGrid . className = 'flex gap-1' ;
224
+
225
+ weeks . forEach ( week => {
226
+ const weekContainer = document . createElement ( 'div' ) ;
227
+ weekContainer . className = 'flex flex-col gap-1' ;
228
+
229
+ week . forEach ( ( { date, count } ) => {
230
+ const cell = document . createElement ( 'div' ) ;
231
+ cell . className = `contribution-cell level-${ getContributionLevel ( count ) } ` ;
232
+ cell . title = `${ date } : ${ count } submissions` ;
233
+ weekContainer . appendChild ( cell ) ;
234
+ } ) ;
235
+
236
+ contributionGrid . appendChild ( weekContainer ) ;
237
+ } ) ;
238
+
239
+ gridContainer . appendChild ( contributionGrid ) ;
240
+ container . appendChild ( gridContainer ) ;
241
+
242
+ // Add legend
243
+ const legendContainer = document . createElement ( 'div' ) ;
244
+ legendContainer . className = 'flex items-center gap-2 text-xs text-gray-400 mt-4' ;
245
+ legendContainer . innerHTML = `
246
+ <span>Less</span>
247
+ <div class="flex gap-1">
248
+ <div class="contribution-cell level-0"></div>
249
+ <div class="contribution-cell level-1"></div>
250
+ <div class="contribution-cell level-2"></div>
251
+ <div class="contribution-cell level-3"></div>
252
+ <div class="contribution-cell level-4"></div>
253
+ </div>
254
+ <span>More</span>
255
+ ` ;
256
+ container . appendChild ( legendContainer ) ;
257
+
258
+ heatmapContainer . appendChild ( container ) ;
259
+ }
260
+
261
+ function processCalendarData ( submissionCalendar ) {
262
+ const weeks = [ ] ;
263
+ let currentWeek = [ ] ;
264
+
265
+ // Convert timestamps to date objects and sort them
266
+ const dates = Object . keys ( submissionCalendar )
267
+ . map ( timestamp => ( {
268
+ date : new Date ( timestamp * 1000 ) ,
269
+ count : submissionCalendar [ timestamp ]
270
+ } ) )
271
+ . sort ( ( a , b ) => a . date - b . date ) ;
272
+
273
+ // Fill in missing dates with zero contributions
274
+ const startDate = dates [ 0 ] . date ;
275
+ const endDate = dates [ dates . length - 1 ] . date ;
276
+ const dateMap = new Map ( dates . map ( d => [ d . date . toISOString ( ) . split ( 'T' ) [ 0 ] , d . count ] ) ) ;
277
+
278
+ for ( let d = new Date ( startDate ) ; d <= endDate ; d . setDate ( d . getDate ( ) + 1 ) ) {
279
+ const dateStr = d . toISOString ( ) . split ( 'T' ) [ 0 ] ;
280
+ const count = dateMap . get ( dateStr ) || 0 ;
281
+ const dayOfWeek = d . getDay ( ) ;
282
+
283
+ if ( dayOfWeek === 0 && currentWeek . length > 0 ) {
284
+ weeks . push ( currentWeek ) ;
285
+ currentWeek = [ ] ;
286
+ }
287
+
288
+ currentWeek . push ( {
289
+ date : dateStr ,
290
+ count : count
291
+ } ) ;
292
+
293
+ if ( currentWeek . length === 7 ) {
294
+ weeks . push ( currentWeek ) ;
295
+ currentWeek = [ ] ;
296
+ }
297
+ }
298
+
299
+ if ( currentWeek . length > 0 ) {
300
+ weeks . push ( currentWeek ) ;
301
+ }
302
+
303
+ return { weeks } ;
304
+ }
305
+
306
+ function getContributionLevel ( count ) {
307
+ if ( count === 0 ) return 0 ;
308
+ if ( count <= 3 ) return 1 ;
309
+ if ( count <= 6 ) return 2 ;
310
+ if ( count <= 9 ) return 3 ;
311
+ return 4 ;
312
+ }
313
+ function displayRecentSubmissions ( submissions ) {
314
+ let tableHtml = `
315
+ <h2 class="text-2xl font-bold text-white mb-4">Recent Submissions</h2>
316
+ <div class="overflow-x-auto">
317
+ <table class="w-full rounded-lg overflow-hidden">
318
+ <thead class="bg-white bg-opacity-10">
319
+ <tr>
320
+ <th class="px-4 py-2 text-left">Title</th>
321
+ <th class="px-4 py-2 text-left">Status</th>
322
+ <th class="px-4 py-2 text-left">Language</th>
323
+ <th class="px-4 py-2 text-left">Timestamp</th>
324
+ </tr>
325
+ </thead>
326
+ <tbody>
327
+ ` ;
328
+
329
+ submissions . forEach ( sub => {
330
+ const date = new Date ( sub . timestamp * 1000 ) . toLocaleString ( ) ;
331
+ tableHtml += `
332
+ <tr class="border-t border-white border-opacity-10">
333
+ <td class="px-4 py-2 underline"><a target="_blank" href="https://leetcode.com/problems/${ sub . titleSlug } " target="_blank" rel="noopener noreferrer" class="text-blue-300 hover:text-blue-100">${ sub . title } </a></td>
334
+ <td class="px-4 py-2 font-bold ${ sub . statusDisplay == "Accepted" ? "text-green-400" : "text-red-400" } ">${ sub . statusDisplay } </td>
335
+ <td class="px-4 py-2">${ sub . lang } </td>
336
+ <td class="px-4 py-2">${ date } </td>
337
+ </tr>
338
+ ` ;
339
+ } ) ;
340
+
341
+ tableHtml += '</tbody></table></div>' ;
342
+ document . getElementById ( 'recentSubmissions' ) . innerHTML = tableHtml ;
343
+ }
344
+ </ script >
345
+ </ body >
346
+ </ html >
0 commit comments