Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ dist
.content-collections

test-results
.claude/CLAUDE.md
4 changes: 4 additions & 0 deletions src/components/DocsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@
}

// Helper to get text color class from framework badge
const getFrameworkTextColor = (frameworkValue: string | undefined) => {

Check warning on line 258 in src/components/DocsLayout.tsx

View workflow job for this annotation

GitHub Actions / PR

'getFrameworkTextColor' is assigned a value but never used. Allowed unused vars must match /(^_)|(^__+$)|(^e$)|(^error$)/u
if (!frameworkValue) return 'text-gray-500'
const framework = frameworkOptions.find((f) => f.value === frameworkValue)

Expand Down Expand Up @@ -378,6 +378,10 @@
label: 'Contributors',
to: '/$libraryId/$version/docs/contributors',
},
{
label: 'NPM Stats',
to: '/$libraryId/$version/docs/npm-stats',
},
...(config.sections.find((d) => d.label === 'Community Resources')
? [
{
Expand Down
121 changes: 121 additions & 0 deletions src/components/NpmStatsSummaryBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Suspense } from 'react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { BlankErrorBoundary } from './BlankErrorBoundary'
import type { LibrarySlim } from '~/libraries'
import { ossStatsQuery, recentDownloadStatsQuery } from '~/queries/stats'
import { useNpmDownloadCounter } from '~/hooks/useNpmDownloadCounter'

function formatNumber(num: number): string {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M'
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K'
}
return num.toLocaleString()
}

function isValidMetric(value: number | undefined | null): boolean {
return (
value !== undefined &&
value !== null &&
!Number.isNaN(value) &&
value >= 0 &&
Number.isFinite(value)
)
}

function NpmStatsSummaryContent({ library }: { library: LibrarySlim }) {
const { data: stats } = useSuspenseQuery(ossStatsQuery({ library }))
const { data: recentStats } = useSuspenseQuery(
recentDownloadStatsQuery({ library }),
)

const npmDownloads = stats.npm?.totalDownloads ?? 0
const hasNpmDownloads = isValidMetric(npmDownloads)

// Use actual data from the API
const dailyDownloads = recentStats?.dailyDownloads ?? 0
const weeklyDownloads = recentStats?.weeklyDownloads ?? 0
const monthlyDownloads = recentStats?.monthlyDownloads ?? 0

// IMPORTANT: useNpmDownloadCounter returns a ref callback, not state
// Must be applied to a DOM element
const counterRef = useNpmDownloadCounter(stats.npm)

return (
<div className="mb-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{/* All Time Downloads (Animated with ref callback) */}
<div className="text-left">
<div className="text-3xl font-bold text-gray-900 dark:text-gray-100 relative">
{hasNpmDownloads ? <span ref={counterRef}>0</span> : <span>0</span>}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 font-medium mt-1">
All Time Downloads
</div>
</div>

{/* Monthly Downloads */}
<div className="text-left">
<div className="text-3xl font-bold text-gray-900 dark:text-gray-100">
{formatNumber(monthlyDownloads)}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 font-medium mt-1">
Monthly Downloads
</div>
</div>

{/* Weekly Downloads */}
<div className="text-left">
<div className="text-3xl font-bold text-gray-900 dark:text-gray-100">
{formatNumber(weeklyDownloads)}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 font-medium mt-1">
Weekly Downloads
</div>
</div>

{/* Daily Downloads */}
<div className="text-left">
<div className="text-3xl font-bold text-gray-900 dark:text-gray-100">
{formatNumber(dailyDownloads)}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 font-medium mt-1">
Daily Downloads
</div>
</div>
</div>
</div>
)
}

export default function NpmStatsSummaryBar({
library,
}: {
library: LibrarySlim
}) {
return (
<Suspense
fallback={
<div className="mb-6">
<div className="animate-pulse">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{Array(4)
.fill(0)
.map((_, i) => (
<div key={i} className="text-left">
<div className="h-9 bg-gray-300 dark:bg-gray-700 rounded w-20 mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-16"></div>
</div>
))}
</div>
</div>
</div>
}
>
<BlankErrorBoundary>
<NpmStatsSummaryContent library={library} />
</BlankErrorBoundary>
</Suspense>
)
}
1 change: 1 addition & 0 deletions src/libraries/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const formProject = {
latestBranch: 'main',
bgRadial: 'from-yellow-500 via-yellow-600/50 to-transparent',
textColor: 'text-yellow-600',
competitors: ['react-hook-form', 'formik', 'react-final-form', 'final-form'],
testimonials: [
{
quote:
Expand Down
1 change: 1 addition & 0 deletions src/libraries/pacer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const pacerProject = {
bgRadial: 'from-lime-500 via-lime-700/50 to-transparent',
textColor: `text-lime-700`,
defaultDocs: 'overview',
competitors: ['lodash.debounce', 'lodash.throttle', 'p-queue', 'bottleneck'],
featureHighlights: [
{
title: 'Flexible & Type-Safe',
Expand Down
1 change: 1 addition & 0 deletions src/libraries/query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const queryProject = {
defaultDocs: 'framework/react/overview',
installPath: 'framework/$framework/installation',
legacyPackages: ['react-query'],
competitors: ['swr', '@apollo/client', 'relay-runtime', '@urql/core'],
handleRedirects: (href: string) => {
handleRedirects(
reactQueryV3List,
Expand Down
1 change: 1 addition & 0 deletions src/libraries/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const routerProject = {
defaultDocs: 'framework/react/overview',
installPath: 'framework/$framework/quick-start',
legacyPackages: ['react-location'],
competitors: ['react-router-dom', '@reach/router', 'next', 'remix'],
hideCodesandboxUrl: true as const,
showVercelUrl: false,
showNetlifyUrl: true,
Expand Down
1 change: 1 addition & 0 deletions src/libraries/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const storeProject = {
bgRadial: 'from-twine-500 via-twine-700/50 to-transparent',
textColor: 'text-twine-700',
defaultDocs: 'overview',
competitors: ['zustand', 'redux', 'mobx', 'jotai', 'valtio'],
featureHighlights: [
{
title: 'Battle-Tested',
Expand Down
1 change: 1 addition & 0 deletions src/libraries/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const tableProject = {
defaultDocs: 'introduction',
corePackageName: 'table-core',
legacyPackages: ['react-table'],
competitors: ['ag-grid-community', '@mui/x-data-grid', 'react-data-grid'],
handleRedirects: (href: string) => {
handleRedirects(
reactTableV7List,
Expand Down
1 change: 1 addition & 0 deletions src/libraries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type LibrarySlim = {
legacyPackages?: string[]
installPath?: string
corePackageName?: string
competitors?: string[]
handleRedirects?: (href: string) => void
/**
* If false, the library is hidden from sidebar navigation and pages have noindex meta tag.
Expand Down
1 change: 1 addition & 0 deletions src/libraries/virtual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const virtualProject = {
textColor: 'text-purple-600',
defaultDocs: 'introduction',
legacyPackages: ['react-virtual'],
competitors: ['react-window', 'react-virtualized', '@tanstack/virtual-core'],
testimonials: [
{
quote:
Expand Down
26 changes: 25 additions & 1 deletion src/queries/stats.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { queryOptions } from '@tanstack/react-query'
import { getOSSStats } from '~/utils/stats.server'
import { getOSSStats, fetchRecentDownloadStats } from '~/utils/stats.server'
import type { StatsQueryParams } from '~/utils/stats.server'
import type { LibrarySlim } from '~/libraries'

Expand All @@ -26,3 +26,27 @@ export function ossStatsQuery({ library }: { library?: LibrarySlim } = {}) {
: undefined,
})
}

export const recentDownloadStatsQueryOptions = (library: LibrarySlim) =>
queryOptions({
queryKey: ['stats', 'recent-downloads', library.id],
queryFn: () =>
fetchRecentDownloadStats({
data: {
library: {
id: library.id,
repo: library.repo,
frameworks: library.frameworks,
},
},
}),
staleTime: 1000 * 60 * 10, // Cache for 10 minutes (fresher than all-time stats)
})

export function recentDownloadStatsQuery({
library,
}: {
library: LibrarySlim
}) {
return recentDownloadStatsQueryOptions(library)
}
22 changes: 22 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import { Route as LibraryIdVersionDocsRouteImport } from './routes/$libraryId/$v
import { Route as LibraryIdVersionDocsIndexRouteImport } from './routes/$libraryId/$version.docs.index'
import { Route as ApiAuthCallbackProviderRouteImport } from './routes/api/auth/callback/$provider'
import { Route as LibraryIdVersionDocsChar123Char125DotmdRouteImport } from './routes/$libraryId/$version.docs.{$}[.]md'
import { Route as LibraryIdVersionDocsNpmStatsRouteImport } from './routes/$libraryId/$version.docs.npm-stats'
import { Route as LibraryIdVersionDocsContributorsRouteImport } from './routes/$libraryId/$version.docs.contributors'
import { Route as LibraryIdVersionDocsCommunityResourcesRouteImport } from './routes/$libraryId/$version.docs.community-resources'
import { Route as LibraryIdVersionDocsSplatRouteImport } from './routes/$libraryId/$version.docs.$'
Expand Down Expand Up @@ -513,6 +514,12 @@ const LibraryIdVersionDocsChar123Char125DotmdRoute =
path: '/{$}.md',
getParentRoute: () => LibraryIdVersionDocsRoute,
} as any)
const LibraryIdVersionDocsNpmStatsRoute =
LibraryIdVersionDocsNpmStatsRouteImport.update({
id: '/npm-stats',
path: '/npm-stats',
getParentRoute: () => LibraryIdVersionDocsRoute,
} as any)
const LibraryIdVersionDocsContributorsRoute =
LibraryIdVersionDocsContributorsRouteImport.update({
id: '/contributors',
Expand Down Expand Up @@ -645,6 +652,7 @@ export interface FileRoutesByFullPath {
'/$libraryId/$version/docs/$': typeof LibraryIdVersionDocsSplatRoute
'/$libraryId/$version/docs/community-resources': typeof LibraryIdVersionDocsCommunityResourcesRoute
'/$libraryId/$version/docs/contributors': typeof LibraryIdVersionDocsContributorsRoute
'/$libraryId/$version/docs/npm-stats': typeof LibraryIdVersionDocsNpmStatsRoute
'/$libraryId/$version/docs/{$}.md': typeof LibraryIdVersionDocsChar123Char125DotmdRoute
'/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute
'/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute
Expand Down Expand Up @@ -731,6 +739,7 @@ export interface FileRoutesByTo {
'/$libraryId/$version/docs/$': typeof LibraryIdVersionDocsSplatRoute
'/$libraryId/$version/docs/community-resources': typeof LibraryIdVersionDocsCommunityResourcesRoute
'/$libraryId/$version/docs/contributors': typeof LibraryIdVersionDocsContributorsRoute
'/$libraryId/$version/docs/npm-stats': typeof LibraryIdVersionDocsNpmStatsRoute
'/$libraryId/$version/docs/{$}.md': typeof LibraryIdVersionDocsChar123Char125DotmdRoute
'/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute
'/$libraryId/$version/docs': typeof LibraryIdVersionDocsIndexRoute
Expand Down Expand Up @@ -824,6 +833,7 @@ export interface FileRoutesById {
'/$libraryId/$version/docs/$': typeof LibraryIdVersionDocsSplatRoute
'/$libraryId/$version/docs/community-resources': typeof LibraryIdVersionDocsCommunityResourcesRoute
'/$libraryId/$version/docs/contributors': typeof LibraryIdVersionDocsContributorsRoute
'/$libraryId/$version/docs/npm-stats': typeof LibraryIdVersionDocsNpmStatsRoute
'/$libraryId/$version/docs/{$}.md': typeof LibraryIdVersionDocsChar123Char125DotmdRoute
'/api/auth/callback/$provider': typeof ApiAuthCallbackProviderRoute
'/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute
Expand Down Expand Up @@ -918,6 +928,7 @@ export interface FileRouteTypes {
| '/$libraryId/$version/docs/$'
| '/$libraryId/$version/docs/community-resources'
| '/$libraryId/$version/docs/contributors'
| '/$libraryId/$version/docs/npm-stats'
| '/$libraryId/$version/docs/{$}.md'
| '/api/auth/callback/$provider'
| '/$libraryId/$version/docs/'
Expand Down Expand Up @@ -1004,6 +1015,7 @@ export interface FileRouteTypes {
| '/$libraryId/$version/docs/$'
| '/$libraryId/$version/docs/community-resources'
| '/$libraryId/$version/docs/contributors'
| '/$libraryId/$version/docs/npm-stats'
| '/$libraryId/$version/docs/{$}.md'
| '/api/auth/callback/$provider'
| '/$libraryId/$version/docs'
Expand Down Expand Up @@ -1096,6 +1108,7 @@ export interface FileRouteTypes {
| '/$libraryId/$version/docs/$'
| '/$libraryId/$version/docs/community-resources'
| '/$libraryId/$version/docs/contributors'
| '/$libraryId/$version/docs/npm-stats'
| '/$libraryId/$version/docs/{$}.md'
| '/api/auth/callback/$provider'
| '/$libraryId/$version/docs/'
Expand Down Expand Up @@ -1736,6 +1749,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LibraryIdVersionDocsChar123Char125DotmdRouteImport
parentRoute: typeof LibraryIdVersionDocsRoute
}
'/$libraryId/$version/docs/npm-stats': {
id: '/$libraryId/$version/docs/npm-stats'
path: '/npm-stats'
fullPath: '/$libraryId/$version/docs/npm-stats'
preLoaderRoute: typeof LibraryIdVersionDocsNpmStatsRouteImport
parentRoute: typeof LibraryIdVersionDocsRoute
}
'/$libraryId/$version/docs/contributors': {
id: '/$libraryId/$version/docs/contributors'
path: '/contributors'
Expand Down Expand Up @@ -1799,6 +1819,7 @@ interface LibraryIdVersionDocsRouteChildren {
LibraryIdVersionDocsSplatRoute: typeof LibraryIdVersionDocsSplatRoute
LibraryIdVersionDocsCommunityResourcesRoute: typeof LibraryIdVersionDocsCommunityResourcesRoute
LibraryIdVersionDocsContributorsRoute: typeof LibraryIdVersionDocsContributorsRoute
LibraryIdVersionDocsNpmStatsRoute: typeof LibraryIdVersionDocsNpmStatsRoute
LibraryIdVersionDocsChar123Char125DotmdRoute: typeof LibraryIdVersionDocsChar123Char125DotmdRoute
LibraryIdVersionDocsIndexRoute: typeof LibraryIdVersionDocsIndexRoute
LibraryIdVersionDocsFrameworkIndexRoute: typeof LibraryIdVersionDocsFrameworkIndexRoute
Expand All @@ -1813,6 +1834,7 @@ const LibraryIdVersionDocsRouteChildren: LibraryIdVersionDocsRouteChildren = {
LibraryIdVersionDocsCommunityResourcesRoute:
LibraryIdVersionDocsCommunityResourcesRoute,
LibraryIdVersionDocsContributorsRoute: LibraryIdVersionDocsContributorsRoute,
LibraryIdVersionDocsNpmStatsRoute: LibraryIdVersionDocsNpmStatsRoute,
LibraryIdVersionDocsChar123Char125DotmdRoute:
LibraryIdVersionDocsChar123Char125DotmdRoute,
LibraryIdVersionDocsIndexRoute: LibraryIdVersionDocsIndexRoute,
Expand Down
Loading
Loading