From 3c0f00a765488df84fb92f1748b3b11ea0a7f126 Mon Sep 17 00:00:00 2001 From: kite <254839944+lizhengfeng101@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:38:03 +0800 Subject: [PATCH] feat(pages): show live npm downloads in highlights stats (#794) * feat(pages): show live npm downloads in highlights stats Add a useNpmDownloads hook that fetches real monthly download counts from the npm registry API at runtime, and surface them in the Highlights section, falling back to a static i18n value while loading or on error. Also refresh the stats row copy and ordering, and sync the label/caption/value changes across en, zh, ja and ru. The hook encodes the (scoped) package name into the request URL and aborts the fetch after a timeout so the UI degrades promptly on a slow or unresponsive network. * docs(pages): translate highlights npm-downloads comments to English * fix(pages): give stat3 a distinct caption and refactor npm-downloads hook - Give highlights stat3 its own caption instead of duplicating stat1's 'battle-tested' text across all four locales - Correct the Russian stat3 label to match 'real-world tasks' in the other locales - Refactor the npm-downloads fetch to async/await and clear the timeout in a finally block so it no longer lingers after the request settles --- pages/src/components/HighlightsSection.tsx | 20 ++++++- pages/src/hooks/useNpmDownloads.ts | 66 ++++++++++++++++++++++ pages/src/i18n/en.ts | 12 ++-- pages/src/i18n/ja.ts | 12 ++-- pages/src/i18n/ru.ts | 14 ++--- pages/src/i18n/zh.ts | 10 ++-- 6 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 pages/src/hooks/useNpmDownloads.ts diff --git a/pages/src/components/HighlightsSection.tsx b/pages/src/components/HighlightsSection.tsx index 2848141..cd9afc2 100644 --- a/pages/src/components/HighlightsSection.tsx +++ b/pages/src/components/HighlightsSection.tsx @@ -4,6 +4,17 @@ import React, { useRef, useEffect, useState } from 'react'; import { useTranslation } from '../i18n'; import { useResponsive } from '../hooks/useResponsive'; +import { useNpmDownloads } from '../hooks/useNpmDownloads'; + +// npm package name, used to fetch real external download counts +const NPM_PACKAGE = '@alibaba-group/open-code-review'; + +// Compress the download count into a compact format consistent with the other stats: 149176 -> "149K+" +function formatNpmDownloads(n: number): string { + if (n >= 1_000_000) return `${Math.floor(n / 1_000_000)}M+`; + if (n >= 1_000) return `${Math.floor(n / 1_000)}K+`; + return `${n}`; +} // 从字符串中解析数字和前后缀 function parseStatValue(value: string): { prefix: string; number: number; suffix: string } { @@ -57,6 +68,8 @@ const CountUpValue: React.FC<{ value: string; isVisible: boolean }> = ({ value, const HighlightsSection: React.FC = () => { const { t } = useTranslation(); const { isMobile, isTablet } = useResponsive(); + // Fetch live monthly npm downloads, reflecting real external community usage + const npm = useNpmDownloads(NPM_PACKAGE, 'last-month'); const sectionRef = useRef(null); const [isVisible, setIsVisible] = useState(false); @@ -78,8 +91,13 @@ const HighlightsSection: React.FC = () => { const stats = [ { value: t('highlights.stat1Value'), label: t('highlights.stat1Label'), caption: t('highlights.stat1Caption') }, - { value: t('highlights.stat2Value'), label: t('highlights.stat2Label'), caption: t('highlights.stat2Caption') }, { value: t('highlights.stat3Value'), label: t('highlights.stat3Label'), caption: t('highlights.stat3Caption') }, + { + // Live monthly npm downloads; fall back to the static i18n value while loading or on failure + value: npm.downloads !== null ? formatNpmDownloads(npm.downloads) : t('highlights.stat2Value'), + label: t('highlights.stat2Label'), + caption: t('highlights.stat2Caption'), + }, { value: t('highlights.stat4Value'), label: t('highlights.stat4Label'), caption: t('highlights.stat4Caption') }, { value: t('highlights.stat5Value'), label: t('highlights.stat5Label'), caption: t('highlights.stat5Caption') }, ]; diff --git a/pages/src/hooks/useNpmDownloads.ts b/pages/src/hooks/useNpmDownloads.ts new file mode 100644 index 0000000..ece1956 --- /dev/null +++ b/pages/src/hooks/useNpmDownloads.ts @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 alibaba/open-code-review Contributors + +import { useEffect, useState } from 'react'; + +type Period = 'last-day' | 'last-week' | 'last-month' | 'last-year'; + +interface NpmDownloadsState { + downloads: number | null; + loading: boolean; + error: boolean; +} + +/** + * Fetch the live download count for an npm package. + * The data source is the official npm stats API (CORS-enabled, callable directly from a purely static page). + * When the request fails, `error` is true so callers can degrade gracefully. + * + * Currently used by the "NPM community downloads" stat in HighlightsSection: while the + * request is in flight or has failed, the component falls back to the static i18n value. + */ +export function useNpmDownloads(pkg: string, period: Period = 'last-month'): NpmDownloadsState { + const [state, setState] = useState({ + downloads: null, + loading: true, + error: false, + }); + + useEffect(() => { + let cancelled = false; + setState({ downloads: null, loading: true, error: false }); + + // On a slow network or an unresponsive API, abort the request after a timeout and degrade, + // so the UI does not stay stuck in the loading state indefinitely + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 8000); + + (async () => { + try { + // pkg may be a scoped package name (containing `/`), so encode it before interpolation to keep the URL path valid + const r = await fetch(`https://api.npmjs.org/downloads/point/${period}/${encodeURIComponent(pkg)}`, { + signal: controller.signal, + }); + if (!r.ok) throw new Error(`npm downloads API responded ${r.status}`); + const data: { downloads?: number } = await r.json(); + if (cancelled) return; + if (typeof data.downloads !== 'number') throw new Error('unexpected payload'); + setState({ downloads: data.downloads, loading: false, error: false }); + } catch { + if (cancelled) return; + setState({ downloads: null, loading: false, error: true }); + } finally { + // Clear the timeout as soon as the request settles, so it doesn't linger and abort a finished request + clearTimeout(timeout); + } + })(); + + return () => { + cancelled = true; + clearTimeout(timeout); + controller.abort(); + }; + }, [pkg, period]); + + return state; +} diff --git a/pages/src/i18n/en.ts b/pages/src/i18n/en.ts index b2d85b8..b2ed982 100644 --- a/pages/src/i18n/en.ts +++ b/pages/src/i18n/en.ts @@ -25,14 +25,14 @@ export const en = { // Highlights 'highlights.stat1Value': '20K+', - 'highlights.stat1Label': 'ACTIVE USERS', + 'highlights.stat1Label': 'INTERNAL ACTIVE USERS', 'highlights.stat1Caption': 'Battle-tested inside Alibaba Group', - 'highlights.stat2Value': '> 30%', - 'highlights.stat2Label': 'ADOPTION RATE', - 'highlights.stat2Caption': 'Battle-tested inside Alibaba Group', - 'highlights.stat3Value': '1M+', + 'highlights.stat2Value': '150K+', + 'highlights.stat2Label': 'NPM COMMUNITY DOWNLOADS', + 'highlights.stat2Caption': 'Real npm downloads · last 30 days', + 'highlights.stat3Value': '3M+', 'highlights.stat3Label': 'REAL-WORLD TASKS', - 'highlights.stat3Caption': 'Code review tasks executed', + 'highlights.stat3Caption': 'Code review tasks executed to date', 'highlights.stat4Value': '1/9', 'highlights.stat4Label': 'TOKEN COST', 'highlights.stat4Caption': 'vs. Claude Code · 1,000 PRs', diff --git a/pages/src/i18n/ja.ts b/pages/src/i18n/ja.ts index 2bb1336..529fa69 100644 --- a/pages/src/i18n/ja.ts +++ b/pages/src/i18n/ja.ts @@ -27,14 +27,14 @@ export const ja: TranslationKeys = { // Highlights 'highlights.stat1Value': '20K+', - 'highlights.stat1Label': 'アクティブユーザー', + 'highlights.stat1Label': '社内アクティブユーザー', 'highlights.stat1Caption': 'Alibabaグループ内で実戦検証済み', - 'highlights.stat2Value': '> 30%', - 'highlights.stat2Label': '採用率', - 'highlights.stat2Caption': 'Alibabaグループ内で実戦検証済み', - 'highlights.stat3Value': '1M+', + 'highlights.stat2Value': '150K+', + 'highlights.stat2Label': 'NPM コミュニティダウンロード', + 'highlights.stat2Caption': 'npm 過去30日の実ダウンロード数', + 'highlights.stat3Value': '3M+', 'highlights.stat3Label': '実タスク', - 'highlights.stat3Caption': '実行されたコードレビュータスク', + 'highlights.stat3Caption': '実行済みのコードレビュータスク', 'highlights.stat4Value': '1/9', 'highlights.stat4Label': 'トークンコスト', 'highlights.stat4Caption': 'Claude Code との比較 · 1,000 PR', diff --git a/pages/src/i18n/ru.ts b/pages/src/i18n/ru.ts index f37c60f..ff30df4 100644 --- a/pages/src/i18n/ru.ts +++ b/pages/src/i18n/ru.ts @@ -27,14 +27,14 @@ export const ru: TranslationKeys = { // Highlights 'highlights.stat1Value': '20K+', - 'highlights.stat1Label': 'АКТИВНЫХ ПОЛЬЗОВАТЕЛЕЙ', + 'highlights.stat1Label': 'ВНУТРЕННИЕ АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ', 'highlights.stat1Caption': 'в Alibaba Group', - 'highlights.stat2Value': '> 30%', - 'highlights.stat2Label': 'ЗАМЕЧАНИЙ ПРИНЯТО', - 'highlights.stat2Caption': 'в Alibaba Group', - 'highlights.stat3Value': '1M+', - 'highlights.stat3Label': 'ЗАДАЧ КОД-РЕВЬЮ', - 'highlights.stat3Caption': 'выполнено в реальных проектах', + 'highlights.stat2Value': '150K+', + 'highlights.stat2Label': 'ЗАГРУЗКИ СООБЩЕСТВА NPM', + 'highlights.stat2Caption': 'реальные загрузки npm · 30 дней', + 'highlights.stat3Value': '3M+', + 'highlights.stat3Label': 'РЕАЛЬНЫХ ЗАДАЧ', + 'highlights.stat3Caption': 'Выполненных задач код-ревью', 'highlights.stat4Value': '1/9', 'highlights.stat4Label': 'ОТ РАСХОДА ТОКЕНОВ', 'highlights.stat4Caption': 'Claude Code · 1 000 PR', diff --git a/pages/src/i18n/zh.ts b/pages/src/i18n/zh.ts index 22aa6bc..f94bbdf 100644 --- a/pages/src/i18n/zh.ts +++ b/pages/src/i18n/zh.ts @@ -27,12 +27,12 @@ export const zh: TranslationKeys = { // Highlights 'highlights.stat1Value': '20K+', - 'highlights.stat1Label': '活跃用户', + 'highlights.stat1Label': '内部活跃用户', 'highlights.stat1Caption': '经阿里巴巴集团内部实战验证', - 'highlights.stat2Value': '> 30%', - 'highlights.stat2Label': '采纳率', - 'highlights.stat2Caption': '经阿里巴巴集团内部实战验证', - 'highlights.stat3Value': '1M+', + 'highlights.stat2Value': '150K+', + 'highlights.stat2Label': 'NPM 社区下载量', + 'highlights.stat2Caption': 'npm 近 30 天真实下载', + 'highlights.stat3Value': '3M+', 'highlights.stat3Label': '真实任务', 'highlights.stat3Caption': '已执行的代码审查任务', 'highlights.stat4Value': '1/9',