@@ -73,6 +73,7 @@ interface PyPISnapshot {
7373 package_name : string ;
7474 name : string ;
7575 repo_id : string ;
76+ type : string ;
7677 timestamp : string ;
7778 downloads_last_day : number | null ;
7879 downloads_last_week : number | null ;
@@ -85,6 +86,7 @@ interface PyPISnapshot {
8586interface PyPIPackageHistory {
8687 name : string ;
8788 repo_id : string ;
89+ type : string ;
8890 snapshots : PyPISnapshot [ ] ;
8991}
9092
@@ -97,6 +99,7 @@ interface PyPIMetrics {
9799 package_name : string ;
98100 name : string ;
99101 repo_id : string ;
102+ type : string ;
100103 downloads_last_day : number ;
101104 downloads_last_week : number ;
102105 downloads_last_month : number ;
@@ -108,6 +111,7 @@ type SortColumn = "name" | "language" | "stars" | "forks" | "unique_visitors" |
108111type PyPISortColumn = "name" | "downloads_last_day" | "downloads_last_week" | "downloads_last_month" | "version" ;
109112type SortDirection = "asc" | "desc" ;
110113type ActiveTab = "github" | "pypi" ;
114+ type PyPIFilter = "all" | "tool" | "bootcamp" ;
111115
112116export default function AnalyticsPage ( ) {
113117 // Load data dynamically to ensure fresh data during development
@@ -120,6 +124,7 @@ export default function AnalyticsPage() {
120124 const [ sortDirection , setSortDirection ] = useState < SortDirection > ( "desc" ) ;
121125 const [ pypiSortDirection , setPypiSortDirection ] = useState < SortDirection > ( "desc" ) ;
122126 const [ activeTab , setActiveTab ] = useState < ActiveTab > ( "github" ) ;
127+ const [ pypiFilter , setPypiFilter ] = useState < PyPIFilter > ( "all" ) ;
123128
124129 useEffect ( ( ) => {
125130 const loadData = async ( ) => {
@@ -296,6 +301,7 @@ export default function AnalyticsPage() {
296301 package_name,
297302 name : pkg . name ,
298303 repo_id : pkg . repo_id ,
304+ type : pkg . type || "tool" , // Default to "tool" for backward compatibility
299305 downloads_last_day : latest . downloads_last_day || 0 ,
300306 downloads_last_week : latest . downloads_last_week || 0 ,
301307 downloads_last_month : latest . downloads_last_month || 0 ,
@@ -306,17 +312,23 @@ export default function AnalyticsPage() {
306312 . filter ( ( p ) : p is PyPIMetrics => p !== null ) ;
307313 } , [ pypiData , repoDescriptions ] ) ;
308314
309- // Calculate aggregate PyPI metrics
315+ // Filter PyPI metrics based on selected filter
316+ const filteredPypiMetrics = useMemo ( ( ) => {
317+ if ( pypiFilter === "all" ) return allPypiMetrics ;
318+ return allPypiMetrics . filter ( ( pkg ) => pkg . type === pypiFilter ) ;
319+ } , [ allPypiMetrics , pypiFilter ] ) ;
320+
321+ // Calculate aggregate PyPI metrics (using filtered data)
310322 const aggregatePypiMetrics = useMemo ( ( ) => {
311- const totalDownloadsDay = allPypiMetrics . reduce (
323+ const totalDownloadsDay = filteredPypiMetrics . reduce (
312324 ( sum , p ) => sum + p . downloads_last_day ,
313325 0
314326 ) ;
315- const totalDownloadsWeek = allPypiMetrics . reduce (
327+ const totalDownloadsWeek = filteredPypiMetrics . reduce (
316328 ( sum , p ) => sum + p . downloads_last_week ,
317329 0
318330 ) ;
319- const totalDownloadsMonth = allPypiMetrics . reduce (
331+ const totalDownloadsMonth = filteredPypiMetrics . reduce (
320332 ( sum , p ) => sum + p . downloads_last_month ,
321333 0
322334 ) ;
@@ -325,32 +337,32 @@ export default function AnalyticsPage() {
325337 totalDownloadsDay,
326338 totalDownloadsWeek,
327339 totalDownloadsMonth,
328- totalPackages : allPypiMetrics . length ,
340+ totalPackages : filteredPypiMetrics . length ,
329341 avgDownloadsPerPackage :
330- allPypiMetrics . length > 0
331- ? Math . round ( totalDownloadsMonth / allPypiMetrics . length )
342+ filteredPypiMetrics . length > 0
343+ ? Math . round ( totalDownloadsMonth / filteredPypiMetrics . length )
332344 : 0 ,
333345 } ;
334- } , [ allPypiMetrics ] ) ;
346+ } , [ filteredPypiMetrics ] ) ;
335347
336- // Get top PyPI performers
348+ // Get top PyPI performers (using filtered data)
337349 const topPypiPerformers = useMemo ( ( ) => {
338350 return {
339- byDay : [ ...allPypiMetrics ]
351+ byDay : [ ...filteredPypiMetrics ]
340352 . sort ( ( a , b ) => b . downloads_last_day - a . downloads_last_day )
341353 . slice ( 0 , 5 ) ,
342- byWeek : [ ...allPypiMetrics ]
354+ byWeek : [ ...filteredPypiMetrics ]
343355 . sort ( ( a , b ) => b . downloads_last_week - a . downloads_last_week )
344356 . slice ( 0 , 5 ) ,
345- byMonth : [ ...allPypiMetrics ]
357+ byMonth : [ ...filteredPypiMetrics ]
346358 . sort ( ( a , b ) => b . downloads_last_month - a . downloads_last_month )
347359 . slice ( 0 , 5 ) ,
348360 } ;
349- } , [ allPypiMetrics ] ) ;
361+ } , [ filteredPypiMetrics ] ) ;
350362
351- // Sort PyPI metrics
363+ // Sort PyPI metrics (using filtered data)
352364 const sortedPypiMetrics = useMemo ( ( ) => {
353- const sorted = [ ...allPypiMetrics ] . sort ( ( a , b ) => {
365+ const sorted = [ ...filteredPypiMetrics ] . sort ( ( a , b ) => {
354366 let aValue : string | number | null = a [ pypiSortColumn ] ;
355367 let bValue : string | number | null = b [ pypiSortColumn ] ;
356368
@@ -372,7 +384,7 @@ export default function AnalyticsPage() {
372384 } ) ;
373385
374386 return sorted ;
375- } , [ allPypiMetrics , pypiSortColumn , pypiSortDirection ] ) ;
387+ } , [ filteredPypiMetrics , pypiSortColumn , pypiSortDirection ] ) ;
376388
377389 // Handle PyPI column header click
378390 const handlePypiSort = ( column : PyPISortColumn ) => {
@@ -771,10 +783,62 @@ export default function AnalyticsPage() {
771783 Downloads include CI/CD pipelines, automated builds, dependency installations, and development environment setups.
772784 A single user or organization typically generates many downloads through automation and tooling.
773785 </ p >
786+ < p className = "text-blue-800 dark:text-blue-200 mt-2" >
787+ < span className = "font-medium" > Tool packages</ span > are production-ready libraries for use in projects.
788+ < span className = "font-medium ml-2" > Bootcamp packages</ span > are educational utilities for tutorials and demos.
789+ </ p >
774790 </ div >
775791 </ div >
776792 </ div >
777793
794+ { /* Filter Buttons */ }
795+ < div className = "mb-8 flex items-center gap-3" >
796+ < span className = "text-sm font-medium text-gray-700 dark:text-gray-300" >
797+ Filter by type:
798+ </ span >
799+ < div className = "flex gap-2" >
800+ < button
801+ onClick = { ( ) => setPypiFilter ( "all" ) }
802+ className = { `px-4 py-2 rounded-lg text-sm font-medium transition-all ${
803+ pypiFilter === "all"
804+ ? "bg-vector-magenta text-white shadow-md"
805+ : "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
806+ } `}
807+ >
808+ All Packages
809+ < span className = "ml-2 text-xs opacity-75" >
810+ ({ allPypiMetrics . length } )
811+ </ span >
812+ </ button >
813+ < button
814+ onClick = { ( ) => setPypiFilter ( "tool" ) }
815+ className = { `px-4 py-2 rounded-lg text-sm font-medium transition-all ${
816+ pypiFilter === "tool"
817+ ? "bg-vector-magenta text-white shadow-md"
818+ : "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
819+ } `}
820+ >
821+ Tool
822+ < span className = "ml-2 text-xs opacity-75" >
823+ ({ allPypiMetrics . filter ( ( p ) => p . type === "tool" ) . length } )
824+ </ span >
825+ </ button >
826+ < button
827+ onClick = { ( ) => setPypiFilter ( "bootcamp" ) }
828+ className = { `px-4 py-2 rounded-lg text-sm font-medium transition-all ${
829+ pypiFilter === "bootcamp"
830+ ? "bg-vector-magenta text-white shadow-md"
831+ : "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
832+ } `}
833+ >
834+ Bootcamp
835+ < span className = "ml-2 text-xs opacity-75" >
836+ ({ allPypiMetrics . filter ( ( p ) => p . type === "bootcamp" ) . length } )
837+ </ span >
838+ </ button >
839+ </ div >
840+ </ div >
841+
778842 { /* Key Metrics */ }
779843 < section className = "mb-12" >
780844 < h2 className = "text-2xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2" >
@@ -909,15 +973,20 @@ export default function AnalyticsPage() {
909973 className = "px-6 py-4 whitespace-nowrap relative"
910974 title = { pkg . description }
911975 >
912- < a
913- href = { `https://pypi.org/project/${ pkg . package_name } /` }
914- target = "_blank"
915- rel = "noopener noreferrer"
916- 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"
917- >
918- { pkg . package_name }
919- < ExternalLink className = "w-3 h-3" />
920- </ a >
976+ < div >
977+ < a
978+ href = { `https://pypi.org/project/${ pkg . package_name } /` }
979+ target = "_blank"
980+ rel = "noopener noreferrer"
981+ 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"
982+ >
983+ { pkg . package_name }
984+ < ExternalLink className = "w-3 h-3" />
985+ </ a >
986+ < div className = "mt-1 text-xs text-gray-500 dark:text-gray-400" >
987+ { pkg . type === "tool" ? "Tool" : "Bootcamp" }
988+ </ div >
989+ </ div >
921990 { pkg . description && (
922991 < 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" >
923992 { pkg . description }
0 commit comments