@@ -12,6 +12,9 @@ import {
1212 Award ,
1313 ExternalLink ,
1414 BarChart3 ,
15+ ArrowUpDown ,
16+ ArrowUp ,
17+ ArrowDown ,
1518} from "lucide-react" ;
1619import Image from "next/image" ;
1720import { getAssetPath } from "@/lib/utils" ;
@@ -54,12 +57,24 @@ interface RepoMetrics {
5457 unique_visitors : number ;
5558 unique_cloners : number ;
5659 language : string | null ;
60+ description ?: string ;
5761}
5862
63+ interface RepositoryInfo {
64+ repo_id : string ;
65+ description : string ;
66+ }
67+
68+ type SortColumn = "name" | "language" | "stars" | "forks" | "unique_visitors" | "unique_cloners" ;
69+ type SortDirection = "asc" | "desc" ;
70+
5971export default function AnalyticsPage ( ) {
6072 // Load data dynamically to ensure fresh data during development
6173 const [ historicalData , setHistoricalData ] = useState < HistoricalData | null > ( null ) ;
6274 const [ isLoading , setIsLoading ] = useState ( true ) ;
75+ const [ repoDescriptions , setRepoDescriptions ] = useState < Record < string , string > > ( { } ) ;
76+ const [ sortColumn , setSortColumn ] = useState < SortColumn > ( "unique_cloners" ) ;
77+ const [ sortDirection , setSortDirection ] = useState < SortDirection > ( "desc" ) ;
6378
6479 useEffect ( ( ) => {
6580 const loadData = async ( ) => {
@@ -68,12 +83,29 @@ export default function AnalyticsPage() {
6883 process . env . NEXT_PUBLIC_BASE_PATH === "true"
6984 ? "/implementation-catalog"
7085 : "" ;
71- const response = await fetch (
86+
87+ // Load historical metrics data
88+ const metricsResponse = await fetch (
7289 `${ basePath } /data/github_metrics_history.json`
7390 ) ;
74- if ( ! response . ok ) throw new Error ( "Failed to fetch metrics data" ) ;
75- const data = await response . json ( ) ;
76- setHistoricalData ( data ) ;
91+ if ( ! metricsResponse . ok ) throw new Error ( "Failed to fetch metrics data" ) ;
92+ const metricsData = await metricsResponse . json ( ) ;
93+ setHistoricalData ( metricsData ) ;
94+
95+ // Load repository descriptions
96+ try {
97+ const reposResponse = await fetch ( `${ basePath } /data/repositories.json` ) ;
98+ if ( reposResponse . ok ) {
99+ const reposData = await reposResponse . json ( ) ;
100+ const descriptions : Record < string , string > = { } ;
101+ reposData . repositories ?. forEach ( ( repo : RepositoryInfo ) => {
102+ descriptions [ repo . repo_id ] = repo . description ;
103+ } ) ;
104+ setRepoDescriptions ( descriptions ) ;
105+ }
106+ } catch ( error ) {
107+ console . warn ( "No repository descriptions found:" , error ) ;
108+ }
77109 } catch ( error ) {
78110 console . warn ( "No historical metrics data found:" , error ) ;
79111 setHistoricalData ( null ) ;
@@ -101,10 +133,11 @@ export default function AnalyticsPage() {
101133 unique_visitors : latest . unique_visitors_14d || 0 ,
102134 unique_cloners : latest . unique_cloners_14d || 0 ,
103135 language : latest . language ,
136+ description : repoDescriptions [ repo_id ] ,
104137 } as RepoMetrics ;
105138 } )
106139 . filter ( ( r ) : r is RepoMetrics => r !== null ) ;
107- } , [ historicalData ] ) ;
140+ } , [ historicalData , repoDescriptions ] ) ;
108141
109142 // Calculate aggregate metrics
110143 const aggregateMetrics = useMemo ( ( ) => {
@@ -145,6 +178,54 @@ export default function AnalyticsPage() {
145178 } ;
146179 } , [ allRepoMetrics ] ) ;
147180
181+ // Sort repository metrics
182+ const sortedRepoMetrics = useMemo ( ( ) => {
183+ const sorted = [ ...allRepoMetrics ] . sort ( ( a , b ) => {
184+ let aValue : string | number | null = a [ sortColumn ] ;
185+ let bValue : string | number | null = b [ sortColumn ] ;
186+
187+ // Handle null/undefined values
188+ if ( aValue === null || aValue === undefined ) aValue = "" ;
189+ if ( bValue === null || bValue === undefined ) bValue = "" ;
190+
191+ // For strings, use locale compare
192+ if ( typeof aValue === "string" && typeof bValue === "string" ) {
193+ return sortDirection === "asc"
194+ ? aValue . localeCompare ( bValue )
195+ : bValue . localeCompare ( aValue ) ;
196+ }
197+
198+ // For numbers
199+ return sortDirection === "asc"
200+ ? ( aValue as number ) - ( bValue as number )
201+ : ( bValue as number ) - ( aValue as number ) ;
202+ } ) ;
203+
204+ return sorted ;
205+ } , [ allRepoMetrics , sortColumn , sortDirection ] ) ;
206+
207+ // Handle column header click
208+ const handleSort = ( column : SortColumn ) => {
209+ if ( sortColumn === column ) {
210+ setSortDirection ( sortDirection === "asc" ? "desc" : "asc" ) ;
211+ } else {
212+ setSortColumn ( column ) ;
213+ setSortDirection ( "desc" ) ;
214+ }
215+ } ;
216+
217+ // Get sort icon for a column
218+ const getSortIcon = ( column : SortColumn ) => {
219+ if ( sortColumn !== column ) {
220+ return < ArrowUpDown className = "w-3 h-3 opacity-50" /> ;
221+ }
222+ return sortDirection === "asc" ? (
223+ < ArrowUp className = "w-3 h-3" />
224+ ) : (
225+ < ArrowDown className = "w-3 h-3" />
226+ ) ;
227+ } ;
228+
148229 return (
149230 < div className = "min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 dark:from-gray-900 dark:via-gray-900 dark:to-gray-800" >
150231 { /* Header */ }
@@ -306,60 +387,95 @@ export default function AnalyticsPage() {
306387 < table className = "w-full" >
307388 < thead className = "bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700" >
308389 < tr >
309- < th className = "px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" >
310- Repository
390+ < th
391+ className = "px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
392+ onClick = { ( ) => handleSort ( "name" ) }
393+ >
394+ < div className = "flex items-center gap-2" >
395+ Repository
396+ { getSortIcon ( "name" ) }
397+ </ div >
311398 </ th >
312- < th className = "px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" >
313- Language
399+ < th
400+ className = "px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
401+ onClick = { ( ) => handleSort ( "language" ) }
402+ >
403+ < div className = "flex items-center gap-2" >
404+ Language
405+ { getSortIcon ( "language" ) }
406+ </ div >
314407 </ th >
315- < th className = "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" >
408+ < th
409+ className = "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
410+ onClick = { ( ) => handleSort ( "stars" ) }
411+ >
316412 < div className = "flex items-center justify-end gap-1" >
317413 < Star className = "w-3 h-3" />
318414 Stars
415+ { getSortIcon ( "stars" ) }
319416 </ div >
320417 </ th >
321- < th className = "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" >
418+ < th
419+ className = "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
420+ onClick = { ( ) => handleSort ( "forks" ) }
421+ >
322422 < div className = "flex items-center justify-end gap-1" >
323423 < GitFork className = "w-3 h-3" />
324424 Forks
425+ { getSortIcon ( "forks" ) }
325426 </ div >
326427 </ th >
327- < th className = "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" >
428+ < th
429+ className = "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
430+ onClick = { ( ) => handleSort ( "unique_visitors" ) }
431+ >
328432 < div className = "flex items-center justify-end gap-1" >
329433 < Eye className = "w-3 h-3" />
330434 Visitors (14d)
435+ { getSortIcon ( "unique_visitors" ) }
331436 </ div >
332437 </ th >
333- < th className = "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" >
438+ < th
439+ className = "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
440+ onClick = { ( ) => handleSort ( "unique_cloners" ) }
441+ >
334442 < div className = "flex items-center justify-end gap-1" >
335443 < Download className = "w-3 h-3" />
336444 Cloners (14d)
445+ { getSortIcon ( "unique_cloners" ) }
337446 </ div >
338447 </ th >
339448 </ tr >
340449 </ thead >
341450 < tbody className = "divide-y divide-gray-200 dark:divide-gray-700" >
342- { [ ...allRepoMetrics ]
343- . sort ( ( a , b ) => b . unique_cloners - a . unique_cloners )
344- . map ( ( repo , index ) => (
345- < motion . tr
346- key = { repo . repo_id }
347- initial = { { opacity : 0 , y : 10 } }
348- animate = { { opacity : 1 , y : 0 } }
349- transition = { { duration : 0.3 , delay : index * 0.02 } }
350- className = "hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors"
451+ { sortedRepoMetrics . map ( ( repo , index ) => (
452+ < motion . tr
453+ key = { repo . repo_id }
454+ initial = { { opacity : 0 , y : 10 } }
455+ animate = { { opacity : 1 , y : 0 } }
456+ transition = { { duration : 0.3 , delay : index * 0.02 } }
457+ className = "hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors group"
458+ >
459+ < td
460+ className = "px-6 py-4 whitespace-nowrap relative"
461+ title = { repo . description }
351462 >
352- < td className = "px-6 py-4 whitespace-nowrap" >
353- < a
354- href = { `https://github.com/${ repo . repo_id } ` }
355- target = "_blank"
356- rel = "noopener noreferrer"
357- className = "flex items-center gap-2 text-sm font-medium text-vector-magenta hover:text-vector-cobalt dark:text-vector-magenta dark:hover:text-vector-cobalt"
358- >
359- { repo . name }
360- < ExternalLink className = "w-3 h-3" />
361- </ a >
362- </ td >
463+ < a
464+ href = { `https://github.com/${ repo . repo_id } ` }
465+ target = "_blank"
466+ rel = "noopener noreferrer"
467+ className = "flex items-center gap-2 text-sm font-medium text-vector-magenta hover:text-vector-cobalt dark:text-vector-magenta dark:hover:text-vector-cobalt"
468+ >
469+ { repo . name }
470+ < ExternalLink className = "w-3 h-3" />
471+ </ a >
472+ { repo . description && (
473+ < div className = "hidden group-hover:block absolute left-0 top-full mt-2 z-50 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm rounded-lg py-3 px-4 w-96 max-w-[calc(100vw-2rem)] shadow-xl border-2 border-gray-200 dark:border-gray-600 leading-relaxed whitespace-normal break-words" >
474+ { repo . description }
475+ < div className = "absolute -top-2 left-8 w-4 h-4 bg-white dark:bg-gray-800 border-l-2 border-t-2 border-gray-200 dark:border-gray-600 transform rotate-45" > </ div >
476+ </ div >
477+ ) }
478+ </ td >
363479 < td className = "px-6 py-4 whitespace-nowrap" >
364480 { repo . language ? (
365481 < span className = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200" >
0 commit comments